Sequência de implementação da jornada de áudios

TLDR: Sete passos para implementar a jornada de áudios diários, de models e migrations até o endpoint GET /audios/daily e os signals de avanço.

Nota: este plano descreve o desenho na época da implementação, em que UserJourney rastreava current_position e unlocked_from. O model evoluiu para current_journey_audio / previous_journey_audio / current_audio_available_from — o estado atual está em reference/audios/daily_audio_journey.md e nas regras R-002 a R-009.

Fases

Passo 1 — Criar os models Journey e JourneyAudio

  • Criar apps/audios/models/journey.py com JourneyKind, JourneyManager, Journey, JourneyAudio e UserJourney
  • Campos de Journey: name, event (CharField, unique, nullable), launch_date (DateField), is_active
  • Exportar em apps/audios/models/__init__.py
  • Gerar e rodar a migration
  • Teste: a migration aplica sem erro

Passo 2 — Criar o model UserJourney

  • Adicionar UserJourney a apps/audios/models/journey.py com os campos:
    • user (OneToOneField → settings.AUTH_USER_MODEL)
    • journey (FK → Journey)
    • current_position (PositiveIntegerField, default=1)
    • unlocked_from (DateField, default=date.today)
    • started_at (DateTimeField, auto_now_add)
    • updated_at (DateTimeField, auto_now)
  • Gerar e rodar a migration
  • Teste: a migration aplica sem erro

Passo 3 — Seed da Journey padrão (data migration)

  • Journey.launch_date deve ser null=True, blank=True — o admin preenche após o deploy
  • Criar a data migration audios/XXXX_seed_default_journey.py
  • Cria uma Journey com name="Jornada Padrão" e launch_date=None
  • Associa os 30 áudios existentes (ordenados por published_at, created_at, id) como JourneyAudio nas posições 1–30
  • Teste: Journey.objects.count() == 1, JourneyAudio.objects.count() == 30

Passo 4 — Registro no admin

  • Registrar Journey em apps/audios/admin.py expondo name, launch_date, is_active
  • Adicionar JourneyAudio como TabularInline dentro do admin da Journey
  • Teste: o admin carrega, permite criar/editar a jornada e associar áudios

Passo 5 — JourneyAudioService (R-002 a R-008)

Escrever os testes primeiro (TDD), depois implementar:

Teste Regra
Usuário sem UserJourney recebe o áudio mais recente fora da jornada R-007 / R-008
Usuário na posição 1 com 0% de progresso recebe o áudio da posição 1 R-002 / R-003
Usuário na posição 1 com progresso concluído recebe o áudio da posição 2 R-004
Usuário na posição 1 que re-escuta áudio já concluído recebe a posição 2 R-005
Usuário além da última posição recebe o áudio mais recente fora da jornada R-006
  • Criar JourneyAudioService em apps/audios/services.py
  • get_daily_audio(user) implementa toda a lógica das regras da jornada
  • Reutiliza AudioProgress de apps/engagements/models/progress.py
  • Conclusão determinada por AudioProgress.completed_at is not None

Cache:

Cenário Key TTL Invalidação
Usuário na jornada audio_journey_{user_id} 5 min (300s) post_save de UserJourney
Áudio livre audio_daily_free até o próximo áudio disponível post_save de Audio

Passo 6 — Signal: criar UserJourney na aceitação dos termos

  • Adicionar o signal em apps/audios/signals.py (não em accounts — evita dependência invertida)
  • No post_save de User, se accept_terms_at foi definido:
    • Busca journey = Journey.objects.filter(kind=JourneyKind.ONBOARDING, is_active=True, launch_date__lte=user.accept_terms_at.date()).order_by('-launch_date').first()
    • Se encontrar, cria UserJourney via get_or_create (idempotente)
  • Teste: aceitar os termos cria UserJourney quando existe jornada ativa com launch_date <= accept_terms_at
  • Teste: não cria UserJourney quando não há jornada ativa

Passo 7 — Atualizar o endpoint GET /audios/daily

  • AudioService.get_daily_audio() passa a receber user como parâmetro
  • Se o usuário tem UserJourney → delega para JourneyAudioService.get_daily_audio(user)
  • Caso contrário → executa o código atual (lógica de janela de 24h, cache audio_daily)
  • Para o caso “usuário além da última posição”, o JourneyAudioService chama AudioService._get_current_daily_audio()
  • Atualizar apps/audios/views.py para passar request.user
  • Adicionar signal post_save de AudioProgress em apps/audios/signals.py: quando completed_at é setado, avança a posição do UserJourney e libera o próximo áudio para o dia seguinte (idempotente)
  • Adicionar signal post_save de UserJourney para invalidar o cache audio_journey_{user_id}
  • GET /audios/daily é puramente read-only — sem escrita no banco

Verificação

  • Teste de integração do endpoint cobrindo todas as regras da jornada (R-002 a R-008)
  • Migrations aplicam sem erro em cada passo
  • Admin permite montar a jornada de ponta a ponta