R-006 — Suspensão vale só para a filiação suspensa

TLDR: Suspender uma filiação marca aquela filiação e mais nada. O perfil do usuário não herda a suspensão, e filiações posteriores do mesmo usuário seguem o fluxo normal. user_profiles.documentation_status fala só de documentação: pending, analysis, ok.

Given / When / Then

Dado um usuário com uma filiação suspensa (por exemplo, por chargeback) e uma filiação posterior paga e aprovada Quando o apolo_membership é consultado para esse usuário Então a resposta reflete a filiação posterior — apolo_access_status: true, suspended: false — e profile.status reflete apenas a documentação do perfil, nunca suspended

Tabela de decisão

Cenário suspended no payload apolo_access_status profile.status
Filiação anterior suspensa + posterior paga e aprovada false true ok
Única filiação paga é a suspensa true false status real da documentação
Duas filiações vigentes, uma suspensa e outra não false (da não suspensa) true status real da documentação
Filiação suspensa + documentação não aprovada true false pending

Restrições

  • A suspensão vive em memberships.suspended / suspended_at, escrita por Membership#suspend! a partir do PAD. É a única fonte da verdade.
  • UserProfile#calculate_documentation_status não consulta filiações suspensas. Derivar isso no nível do usuário fazia uma filiação suspensa antiga contaminar todas as posteriores, e — como o resultado é gravado por before_save :update_cached_fields e pelo after_commit de Membership — a marca era reescrita a cada save, tornando impossível aprovar a documentação pelo admin.
  • O valor suspended continua no enum documentation_status de UserProfile, junto com UserProfile#suspended? e a chave de locale user_profile_documentation_statuses.SUSPENDED. Nada escreve esse valor hoje, mas linhas antigas ainda o carregam: não houve backfill, e removê-lo do enum faria o reader devolver nil e quebrar o admin.
  • Os consumidores externos leem suspensão pelo campo suspended do payload, por filiação — nunca por profile.status.
  • profile.status é gate de PRO no trg-club: Subscriptions::Pro::ValidateEligibility#documentation_approved? exige == "ok" e ExpireWhenIneligible desativa a assinatura quando o resultado é negativo. Foi por esse caminho que o bug tirou os benefícios de quem tinha filiação nova válida. Qualquer mudança nesse campo precisa considerar o trg-club.
  • Membership.active já filtra suspended: false, então User#active_memberships não precisa — e não deve — checar o perfil.

Código

app/models/user_profile.rb

```ruby def calculate_documentation_status return :ok if admin_approved_at return :ok if personal_info_status && attachments_status && admin_approved_at return :analysis if user.current_onboarding&.in_analysis?

:pending end ```

app/models/user.rb

ruby def active_memberships memberships.active.order(created_at: :asc) end

Teste vinculado

test/models/user_profile_test.rb — filiação suspensa não torna o perfil suspenso; filiação anterior suspensa com perfil aprovado resolve :ok test/models/user_test.rb — active_memberships devolve a filiação vigente quando há uma anterior suspensa, e continua vazio quando a própria filiação está suspensa test/serializers/membership_apolo_serializer_test.rb — payload com filiação anterior suspensa sai com suspended: false, apolo_access_status: true e profile.status == "ok"

Histórico

  • 2026-09-01: a derivação foi removida. Uma cliente suspensa por chargeback em dezembro/2025 comprou nova filiação em julho/2026, teve a documentação aprovada e continuou bloqueada, porque has_any_suspended_membership? considerava qualquer filiação suspensa — vencida ou arquivada, de qualquer época. Ver spec.

Relacionados