R-016 — Desbloqueio de aulas para usuário em trial

TLDR: para quem está em trial, o campo unlocked de uma aula depende só do que o trial libera. access_mode=all abre todas as aulas do curso; access_mode=custom abre exatamente as aulas marcadas; nos dois casos a sequência é ignorada, e o resto fica bloqueado.

Given / When / Then

Dado um usuário cujo subscription_status não é "trial" Quando ele consulta aulas Então unlocked sai exatamente como antes — progresso, sequência e override de Lesson.unlocked

Dado um usuário em trial e um curso liberado com access_mode=all Quando ele consulta as aulas desse curso Então todas vêm unlocked: true, mesmo sem nenhuma conclusão anterior e mesmo as que têm Lesson.unlocked=False

Dado um usuário em trial e um curso liberado com access_mode=custom, com as aulas 1, 2 e 7 marcadas Quando ele consulta as aulas desse curso Então as aulas 1, 2 e 7 vêm unlocked: true mesmo sem nenhuma conclusão anterior, e as aulas 3, 4, 5 e 6 vêm unlocked: false

Dado um usuário em trial e uma aula com Lesson.unlocked=True que não foi marcada num curso custom Quando ele consulta essa aula Então ela vem unlocked: false — o trial vence o override

Dado um usuário em trial e um curso que não está em TrialCourse Quando ele consulta as aulas desse curso Então todas vêm unlocked: false, e o curso continua aparecendo nas listagens

Dado um usuário em trial sem nenhum TrialCourse configurado Quando ele consulta qualquer aula Então todas vêm unlocked: false

Dado um usuário em trial cujo subscription_status passa a enabled (conversão) Quando ele consulta as aulas na requisição seguinte Então ele já enxerga o fluxo normal do app, sem esperar a expiração do cache de autenticação

Constraints

  • Quem responde se o usuário é trial é User.is_trial, que compara subscription_status com User.SUBSCRIPTION_STATUS_TRIAL sem diferenciar maiúsculas. A regra vive no model — serviços e serializers apenas consomem, nunca comparam a string por conta própria.
  • access_mode decide quais aulas entram no conjunto liberado, não a semântica do desbloqueio: all entra com todas as aulas ativas do curso, custom entra só com as marcadas em TrialCourseLesson. Nos dois modos, estar no conjunto significa unlocked: true direto.
  • Os dois modos ignoram a sequência de propósito. Em custom, uma aula marcada no meio de um módulo teria o pré-requisito fora do trial, portanto impossível de concluir — a aula liberada pelo admin ficaria inalcançável. Em all, o modo significa literalmente liberar o curso inteiro.
  • resolve_trial_lesson_access devolve um único set de ids de aula, ou None. Não há mais distinção entre conjuntos por modo de acesso.
  • A composição vive em LessonSerializer.get_unlocked. Lesson.is_unlocked() não conhece trial e continua respondendo só por progresso/sequência — para usuário em trial ele não é chamado em nenhum caminho.
  • A resolução roda uma vez por request, guardada em serializer.context['_trial_lesson_access'].
  • None = usuário não é trial (sem restrição); set() vazio = é trial sem nada liberado (bloqueia tudo). Confundir os dois inverte o comportamento para toda a base de assinantes.
  • A decisão lê subscription_status do objeto User que a autenticação cacheia por 24h (user_authentication_{id}). O signal post_save em apps/accounts/signals.py invalida essa chave a cada save — sem isso, quem converte continuaria preso ao acesso de trial por até um dia.
  • Aulas e TrialCourse soft-deleted ou com is_active=False ficam fora do conjunto.
  • Não há bloqueio em listagens: cursos, módulos e trilhas continuam sendo listados por completo. O curso expõe um unlocked próprio, com regra separada desta — ver R-017.
  • SimpleLessonSerializer (continuar assistindo) não expõe unlocked e não passa por esta regra.

Linked test

tests/trials/test_trial_content_service.py tests/trials/test_lesson_unlock_gating.py