Corrigir race na criação da UserJourney ao aceitar termos

TLDR: A criação da UserJourney no aceite de termos é assíncrona (Celery); se o cliente chamar GET /audios/daily antes da task rodar, o usuário recebe o áudio do dia genérico em vez do áudio 1 da jornada. O fix torna a criação síncrona.

Contexto

Ao aceitar os termos, AcceptTermsSerializer.update() (apps/accounts/serializers/account.py) salva accept_terms_at, disparando o signal create_user_journey_on_terms_acceptance (apps/audios/signals.py), que hoje enfileira a task assíncrona create_user_journey.delay(user.pk) (apps/audios/tasks.py). Essa task chama JourneyService.create_for(user) (apps/audios/services.py), que cria a UserJourney e marca is_in_journey=True.

Como a task roda via Celery/RabbitMQ, existe uma janela de corrida: se o cliente chamar GET /audios/daily antes da task ser processada, is_in_journey ainda está False no banco e AudioService.get_daily_audio cai no fallback _get_current_daily_audio() (áudio do dia genérico), em vez de retornar journey.first_audio (áudio 1). No próximo acesso, a task já rodou e o áudio correto aparece — o que bate exatamente com o sintoma relatado: usuário trial vê o áudio do dia na primeira entrada e o áudio 1 na segunda.

Confirmado por grep que create_user_journey (apps/audios/tasks.py) não tem nenhum outro chamador além do signal.

Objetivos

  • Eliminar a race: a UserJourney deve existir e is_in_journey=True antes da resposta do endpoint de aceite de termos retornar ao cliente.
  • Remover a task Celery create_user_journey, que fica órfã após o fix.

Fora de escopo

  • Mudanças no fluxo de advance_user_journey (avanço de posição na jornada) — não relacionado a esta race.
  • Mudanças em JourneyService.create_for ou JourneyService.advance_for além da forma de chamada.
  • Qualquer alteração no client (mobile/web) — o fix é inteiramente no backend.

Mudanças

  • apps/audios/signals.py: no handler create_user_journey_on_terms_acceptance, substituir create_user_journey.delay(instance.pk) por chamada direta e síncrona a JourneyService.create_for(instance).
  • apps/audios/tasks.py: remover a task create_user_journey, agora sem chamadores.
  • Import de create_user_journey em apps/audios/signals.py é substituído pelo import de JourneyService (já existe em apps/audios/services.py).

Como verificar

  • Teste automatizado cobrindo o cenário: usuário aceita os termos (signal disparado) e, na sequência imediata (sem processar nenhuma task Celery), GET /audios/daily retorna o áudio na posição 1 da Journey ativa — não o áudio do dia genérico.
  • Teste do signal handler confirmando que UserJourney é criada de forma síncrona (sem depender de .delay()/worker) quando accept_terms_at é setado.
  • make run.test path=apps/audios deve passar.
  • Rodar make run.ci_local antes de abrir o PR.

Documentação

Atualizar .project/docs/reference/audios/daily_audio_journey.md — a seção que descreve “O UserJourney é criado no momento em que o usuário aceita os termos” deve deixar explícito que a criação é síncrona (via signal, sem Celery), já que a versão atual não menciona o mecanismo e isso motivou o bug.