R-017 — Desbloqueio de cursos para usuário em trial
TLDR: o curso expõe
unlocked, que responde só pelo trial. Quem não é trial vêtrueem tudo; quem está em trial vêtrueapenas nos cursos ligados aoTrialCoursedo seu trial.access_modenão influencia — quais aulas abrem é decisão do R-016.
Given / When / Then
Dado um usuário cujo subscription_status não é "trial"
Quando ele consulta cursos
Então todo curso vem unlocked: true
Dado um usuário em trial e um curso liberado no TrialCourse, com qualquer access_mode
Quando ele consulta cursos
Então esse curso vem unlocked: true
Dado um usuário em trial e um curso que não está no TrialCourse
Quando ele consulta cursos
Então esse curso vem unlocked: false, mas continua aparecendo na listagem e o detalhe dele continua respondendo 200
Dado um usuário em trial sem nenhum TrialCourse configurado
Quando ele consulta cursos
Então todos vêm unlocked: false
Dado um curso não publicado (is_published: false)
Quando qualquer usuário o consulta
Então o unlocked não muda por causa disso — publicação e liberação são eixos independentes
Dado um usuário em trial cujo subscription_status passa a enabled (conversão)
Quando ele consulta cursos na requisição seguinte
Então todos voltam unlocked: true, sem esperar a expiração do cache de autenticação
Constraints
- Quem responde se o usuário é trial é
User.is_trial. A regra vive no model — o serviço e o serializer apenas consomem, nunca comparam a string por conta própria. resolve_trial_course_accessdevolveNonequando o usuário não é trial e umsetdecourse_idcaso contrário.None= sem restrição;setvazio = é trial sem nada liberado (bloqueia tudo). Confundir os dois inverte o comportamento para toda a base de assinantes.- O conjunto não filtra por
access_mode:allecustomsignificam os dois que o curso faz parte do trial. A granularidade decustomé resolvida nounlockedda aula (R-016). - A composição vive no
TrialCourseUnlockMixin, herdado porCourseSerializereCourseModuleSerializer. Cada serializer declara oSerializerMethodFieldno próprio corpo, porque o metaclass do DRF só coleta campos declarados em bases que já são serializers. - A resolução roda uma vez por request, guardada em
serializer.context['_trial_course_access'], independente da quantidade de cursos. - Sem
requestou sem usuário no contexto,unlockedsaitrue— o default do curso é liberado. É o oposto da aula, cujo default é o bloqueio. unlockedé independente depublished_at/is_published: um curso pode viris_published: true, unlocked: falsee vice-versa.- A regra vale em
GET /v1/courses,GET /v1/courses/<id>eGET /v1/trails/<id>/courses— este último porque oTrailCourseSerializeraninhaCourseSerializer. TrialCoursesoft-deleted fica fora do conjunto.- Não há bloqueio em listagens nem no detalhe: nenhum queryset é filtrado, nada responde 403/404 por causa do trial.
- Módulo não tem
unlocked— o trial é configurado por curso, e a aula já tem a trava dela.
Linked test
tests/trials/test_trial_content_service.py
tests/trials/test_course_unlock_gating.py