R-020 — Trial da campanha é travado quando há usuários cadastrados

TLDR: A partir do primeiro UserTrial ativo vinculado à campanha, Campaign.trial não pode mais ser trocado nem esvaziado. Campaign.clean() recusa a mudança e o admin exibe o campo como leitura, com a contagem de usuários e o motivo.

Given / When / Then

Dado uma campanha com trial definido e ao menos um UserTrial ativo com campaign apontando para ela Quando alguém altera Campaign.trial para outro trial (ou para vazio) e chama full_clean() Então é levantado ValidationError no campo trial informando quantos usuários já se cadastraram

Dado a mesma campanha Quando um administrador abre dash.*/admin/campaigns/campaign/<id>/change/ Então trial aparece como texto de leitura e a seção “Informações gerais” exibe o aviso com a contagem de usuários

Dado uma campanha sem trial definido, mesmo com UserTrial vinculados Quando o trial é definido pela primeira vez Então a alteração é permitida — a primeira definição não é uma troca

Dado uma campanha cujo único UserTrial foi soft-deletado Quando o trial é trocado Então a alteração é permitida — a contagem considera apenas registros ativos

Restrições

  • UserTrial grava trial e campaign como colunas independentes: UserTrialService.create_user_trial copia o trial vigente e deriva a janela de trial.duration_days. Trocar o trial da campanha depois não propaga nada
  • Propagar também não seria correto: started_at/expires_at do UserTrial são registro histórico e imutáveis (R-018)
  • Os demais campos da campanha (name, status, start_date, end_date) seguem editáveis com usuários cadastrados
  • Para oferecer outro trial ao mesmo público, crie uma nova campanha
  • A validação vive em Campaign.clean(), chamado pelo ModelForm do admin — não roda em save()/create() diretos por ORM, shell ou outros services. Hoje nenhum código escreve Campaign.trial fora do admin
  • A regra não cobre Trial apagado: Campaign.trial usa on_delete=SET_NULL e esse caminho não passa por clean()

Teste vinculado

tests/campaigns/test_campaign_model.py — test_clean_blocks_trial_swap_when_campaign_has_user_trials, test_clean_blocks_clearing_trial_when_campaign_has_user_trials, test_clean_allows_first_trial_assignment_even_with_user_trials, test_clean_allows_trial_swap_when_user_trial_is_soft_deleted tests/campaigns/test_admin.py — test_trial_is_readonly_when_campaign_has_user_trials, test_fieldset_warns_with_user_trials_count