Lembretes de hábitos não executados

TLDR: Enviar até 4 lembretes de 2 em 2 horas para hábitos não executados no dia, com variação aleatória de mensagem sorteada no primeiro lembrete (8 fluxos), rastreando o horário de cada lembrete em campos dedicados do HabitExecution.

Contexto

Hoje o HabitNotificationService envia apenas uma notificação no horário agendado do hábito. Se o usuário não executar, não recebe nenhum lembrete. A solicitação é enviar até 4 lembretes a cada 2 horas enquanto a execução permanecer pending, com variação aleatória de mensagem (8 fluxos definidos no Figma), usando campos de timestamp para rastrear cada lembrete individualmente.

Objetivos

  • Adicionar os campos first_reminder, second_reminder, third_reminder, fourth_reminder e reminder_variation em HabitExecution
  • Criar a task periódica send_habit_reminders rodando a cada 60s
  • Enviar o lembrete 1 após 2h do scheduled_time, o 2 após 2h do first_reminder, o 3 após 2h do second_reminder e o 4 após 2h do third_reminder
  • No primeiro lembrete, sortear aleatoriamente um dos 8 fluxos de mensagem e persistir em reminder_variation
  • Todos os lembretes subsequentes usam o fluxo sorteado no primeiro
  • Parar os lembretes se o status mudar para completed ou not-completed

Fora de escopo

— (não registrado na spec original)

Mudanças

apps/habits/models/execution.py

Adicionar 5 campos em HabitExecution:

python first_reminder = models.DateTimeField(blank=True, null=True) second_reminder = models.DateTimeField(blank=True, null=True) third_reminder = models.DateTimeField(blank=True, null=True) fourth_reminder = models.DateTimeField(blank=True, null=True) reminder_variation = models.PositiveSmallIntegerField(blank=True, null=True)

apps/habits/migrations/0011_habitexecution_reminders.py (novo)

Migration adicionando os 5 campos nullable.

apps/habits/services.py

Adicionar métodos de lembrete diretamente em ExecutionService:

```python class ExecutionService: REMINDER_INTERVAL = timedelta(hours=2) REMINDER_BODY = “Não se esqueça: {name}”

@classmethod
def send_pending_reminders(cls):
    now = timezone.localtime()
    cls._send_first_reminders(now)
    cls._send_second_reminders(now)
    cls._send_third_reminders(now)
    cls._send_fourth_reminders(now) ```

Lógica por etapa: - Primeiro: status='pending' + notification_ids__len > 0 + first_reminder__isnull=True + scheduled_date+scheduled_time + 2h <= now → sorteia reminder_variation (1-8) - Segundo: first_reminder__isnull=False + second_reminder__isnull=True + first_reminder + 2h <= now - Terceiro: second_reminder__isnull=False + third_reminder__isnull=True + second_reminder + 2h <= now - Quarto: third_reminder__isnull=False + fourth_reminder__isnull=True + third_reminder + 2h <= now

Cada lembrete usa REMINDER_FLOWS[reminder_variation][index] para obter título e corpo. Por etapa: buscar execuções elegíveis com prefetch_related('habit__user__push_devices'), criar todas as Notification em bulk_create e atualizar os timestamps com bulk_update.

apps/habits/tasks.py

python @app.task(...) def send_habit_reminders(): ExecutionService.send_pending_reminders()

config/celery_defaults.py

python 'send-habit-reminders': { 'task': 'apps.habits.tasks.send_habit_reminders', 'schedule': 60.0, },

tests/habits/test_reminder_service.py (novo)

Testes Triple-A cobrindo: cada onda disparando após 2h da anterior; nenhum lembrete antes de 2h; nenhum lembrete após o fourth_reminder; nenhum lembrete se status != 'pending'; nenhum lembrete se notification_ids vazio; enable_notifications=False não envia; reminder_variation entre 1 e 8 no primeiro lembrete; mensagem usando o texto do fluxo sorteado.

Como verificar

  1. python manage.py migrate habits — verificar a migration aplicada
  2. Criar hábito com scheduled_time 2h no passado, status='pending' e notification_ids preenchido — rodar send_habit_reminders → first_reminder preenchido e notificação criada
  3. Executar o hábito (status='completed') — rodar novamente → nenhum lembrete adicional
  4. make test ARGS="tests/habits/test_reminder_service.py -v" — todos passam

Documentação