Trial da campanha é travado quando há usuários cadastrados
TLDR:
Campaign.trialpassa a ser imutável a partir do primeiroUserTrialcadastrado na campanha, porque trocar o trial não atualiza — nem deveria atualizar — osUserTrialjá concedidos.
Contexto
Campaign.trial é um FK opcional e hoje é livremente editável no admin. UserTrial grava trial e campaign como colunas independentes: no cadastro, UserTrialService copia o trial vigente da campanha para o UserTrial e deriva started_at/expires_at a partir de trial.duration_days (janela imutável, R-018).
Consequência: trocar o trial de uma campanha em andamento não propaga nada. Os UserTrial existentes continuam apontando para o trial anterior, com a janela e as permissões do trial anterior, enquanto a campanha passa a exibir outro trial. E propagar também não seria correto — a janela concedida é registro histórico e não pode ser reescrita.
Como não há atualização possível nem desejável, a solução é impedir a troca e explicar o motivo na tela.
Objetivos
- Bloquear qualquer alteração de
Campaign.trialquando a campanha já tem ao menos umUserTrialativo - Exibir no admin uma caixa de aviso explicando por que o campo está travado, com a contagem de usuários cadastrados
- Manter livre a primeira definição do trial (
None→ trial), quando ainda não há cadastros
Fora de escopo
- Migrar ou recalcular
UserTrialexistentes - Alterar o comportamento de
on_delete=SET_NULLquando oTrialem si é apagado - Bloquear a edição de outros campos da campanha (
name,status, datas, slug) — seguem editáveis - Bloquear a edição do próprio
Trial(duração, features), que já tem regras próprias
Regra
| Transição | Sem UserTrial |
Com ao menos 1 UserTrial |
|---|---|---|
None → trial A |
permitido | permitido |
| trial A → trial B | permitido | bloqueado |
trial A → None |
permitido | bloqueado |
A contagem usa o manager default (objects), ou seja apenas UserTrial não soft-deletados — um cadastro removido não trava a campanha.
Mudanças
apps/campaigns/models/campaign.py
- clean() passa a validar a troca do trial: se self.pk existe, o trial_id gravado no banco difere do atual e self.user_trials.exists(), levanta ValidationError({"trial": ...}) com a mensagem explicando que existem usuários cadastrados
- Adiciona helpers has_user_trials e user_trials_count (properties) para o admin reusar a mesma checagem
apps/campaigns/admin.py
- get_readonly_fields acrescenta "trial" quando o objeto em edição já tem UserTrial
- get_fieldsets injeta na seção “Informações gerais” a description com o aviso, renderizado via format_html como caixa destacada (fundo âmbar claro, borda esquerda, fonte 13px): “A troca do trial está travada: N usuário(s) já se cadastraram por esta campanha. Trocar o trial não atualiza os trials já concedidos — crie uma nova campanha para oferecer um trial diferente.”
tests/campaigns/test_campaign_model.py
- Cobertura das três transições da tabela acima, mais o caso soft-deletado e o caso “salvar sem mexer no trial continua permitido”
tests/campaigns/test_admin.py (novo)
- trial está em get_readonly_fields quando há UserTrial, e não está quando não há
- get_fieldsets traz a description de aviso apenas no caso travado, sem mutar o fieldsets de classe
Como verificar
make run.test path=tests/campaigns- No admin (
dash.*/admin/campaigns/campaign/): criar campanha sem trial, salvar, definir um trial → salva normal - Cadastrar um usuário pela campanha (fluxo de registro de trial), reabrir a campanha → campo
Trialaparece como leitura, com a caixa de aviso e a contagem - Via shell (
make run.console):campaign.trial = other; campaign.full_clean()→ValidationErrorno campotrial
Documentação
- Nova regra em
.project/docs/rules/campaigns/campaign_trial_locked_with_user_trials.md(R-020) - Índice
.project/docs/RULES.mdatualizado com a linha R-020