Streak, consistência e totais diários de hábitos

TLDR: Adicionar contagem de streak, nível de consistência e totais diários de conclusão nos endpoints de execução de hábitos.

Contexto

O frontend precisa de um feedback mais rico sobre o progresso dos hábitos. Os endpoints de execução retornavam apenas dados brutos, sem métricas agregadas. Três informações estavam faltando:

  • Quantos dias consecutivos o usuário completou um hábito (streak)
  • Em qual fase de consistência o usuário está (iniciando → consolidado)
  • Quantos hábitos do dia foram concluídos vs o total (resumo diário)

Objetivos

  • Expor streak (dias consecutivos completados) por hábito
  • Expor consistency (nível semântico) por hábito
  • Alterar o response do GET /habits/executions/<date> para incluir as contagens de total e completed de hábitos junto com a lista

Fora de escopo

— (não registrado na spec original)

Mudanças

apps/habits/services.py — HabitDayHistoryService (nova classe)

Tabela de histórico com uma linha por (hábito, data), para evitar recalcular streak/consistência varrendo todas as execuções.

sync_day(habit, date) - Agrega as execuções do dia (total, completed, pending) - Remove a entrada anterior com hard_delete (necessário porque BaseModel usa soft-delete — .delete() apenas seta deleted_at, deixando a linha no banco e violando a constraint única) - Recria com o status correto: in_progress, completed, missed ou partial

sync_habit_range(habit, start_date, end_date) - Versão em lote de sync_day — usada após operações de bulk_create/update - Usa .order_by() antes do .values('scheduled_date').annotate() para evitar que o ordering padrão do model (scheduled_time) entre no GROUP BY e gere múltiplas linhas por data

apps/habits/services.py — ExecutionService

get_streak(habit) - Lê HabitDayHistory ordenado por data DESC - Pula dias in_progress (dia ainda em andamento: não quebra nem conta) - Conta dias completed consecutivos - Para no primeiro dia missed ou partial

get_consistency_level(habit) - total_days = total de linhas em HabitDayHistory até hoje - completed_days = linhas com status='completed' - porcentagem = completed_days / total_days * 100 - Retorna: "starting" (0–20%), "developing" (21–50%), "consistent" (51–80%), "consolidated" (81–100%)

apps/habits/signals.py (novo arquivo)

Signal post_save em HabitExecution → chama HabitDayHistoryService.sync_day automaticamente a cada save individual (ex.: usuário marca execução como concluída).

apps/habits/views.py — ExecutionListByHabitView

Adiciona streak e consistency no response, junto com habit_id e habit_name:

json { "count": 30, "next": null, "previous": null, "results": [], "habit_id": 1, "habit_name": "Meditação", "streak": 5, "consistency": "developing" }

apps/habits/serializers/execution.py — ExecutionSerializer

Adiciona streak e consistency (usado pelo endpoint por data). A view pré-computa os valores por hábito e passa via contexto, para evitar N+1.

apps/habits/views.py — ExecutionListByDateView

Override do list() para envolver o response com os totais diários:

json { "total": 5, "completed": 3, "results": [] }

  • total = quantidade de hábitos distintos com execuções naquela data
  • completed = quantidade de hábitos em que TODAS as execuções do dia estão completed
  • Cada item em results carrega streak e consistency do seu hábito
  • Mudança que quebra contrato — confirmado pelo time de frontend que não há problema

Como verificar

  • Criar hábito com 3 horários por dia
  • Completar os 3 no dia 1 e no dia 2, falhar no dia 3 → streak deve ser 0, não 2
  • Completar 6 de 30 dias esperados → consistency = "starting" (20%)
  • Completar 16 de 30 → consistency = "consistent" (53%)
  • Chamar GET /habits/executions/<date> com 5 hábitos no dia, 3 totalmente concluídos → total=5, completed=3
  • Testes cobrem happy path, reset de streak, cada limiar de consistência e os totais diários

Documentação

Atualizar a documentação de hábitos com as regras de streak e consistência.