R-004 — Elegibilidade PRO exige filiação válida e documentação aprovada

TLDR: A elegibilidade PRO exige duas condições na mesma resposta do CITRG — filiação dentro da validade e documentação aprovada (profile.status == "ok"); antes desta regra só a validade era verificada.

Contexto

O Subscriptions::Pro::ValidateEligibility decidia a elegibilidade PRO a partir de um único campo do payload apolo_membership do CITRG: valid_until_timestamp. Um terapeuta com filiação paga e não expirada continuava PRO mesmo com a documentação nunca aprovada, em análise ou suspensa.

Given / When / Then

Dado um terapeuta em processo de promoção ou revalidação PRO Quando o ValidateEligibility consulta GET apolo_membership no CITRG Então pro_eligible só é true se, na mesma resposta, valid_until_timestamp >= hoje e profile.status == "ok"

Tabela de decisão

Resposta do CITRG Elegível Motivo
valid_until_timestamp >= hoje e profile.status == "ok" sim —
valid_until_timestamp < hoje não expired
profile.status em pending / analysis / suspended não documentation_not_ok
profile ausente do payload não documentation_not_ok (fail closed)
404 (sem usuário ou sem filiação paga) não no_membership
qualquer outra falha, timeout, exceção indefinido citrg_unavailable

A validade já era verificada antes desta mudança; a condição de documentação é o que é novo. As duas são avaliadas sobre o mesmo payload, então uma única consulta responde às duas.

Restrições

  • citrg_unavailable nunca pode rebaixar ninguém. O ExpireWhenIneligible já retorna cedo com context.citrg_unavailable, e o guia de auditoria mantém essas linhas em um balde separado.
  • O status de pagamento deliberadamente não é verificado: apolo_membership só seleciona filiações pagas (scope :paid, -> { where(payment_status: :paid) } na citrg-api, aplicado tanto em valid_paid_memberships quanto em newest_paid_membership), então um 200 sempre carrega payment_status: "paid" e um filiado inadimplente responde 404.
  • A expiração é aplicada em dois lugares, e isso é intencional — não são redundantes:
    • o fluxo (ValidateEligibility, esta regra) pega no sign-in do terapeuta e em POST /api/v1/subscriptions/pros
    • o webhook (POST /citrg/memberships/expired, ver pro_downgrade_on_citrg_expiry) pega os terapeutas que nunca mais fazem login

    Os dois convergem no ExpireWhenIneligible, que é idempotente, então uma dupla execução é inofensiva.

Rebaixamento

Rebaixar para regular-terapeuta exige dois passos, não um:

  1. Subscriptions::Pro::ExpireWhenIneligible — define a assinatura PRO como inactive / expires_at: Time.current e reativa um regular-terapeuta já existente.
  2. Subscriptions::RegularTerapeuta::Creation::CreateSubscription — necessário quando o usuário nunca teve assinatura regular, porque o passo 1 usa find_by(...)&.update! e é no-op nesse caso. Sem ele o terapeuta fica sem nenhum plano ativo.

O índice único em subscriptions (user_id, subscription_plan_id) torna o passo 2 seguro de tentar incondicionalmente, mas o guia o protege mesmo assim para manter o log legível.

Rollout

A base PRO existente foi promovida sob a regra antiga, então terapeutas com documentação não aprovada já estão PRO e só são reavaliados no próximo sign-in. A varredura pontual é um script de console em duas fases (pro_eligibility_audit): a fase 1 audita e imprime, a fase 2 rebaixa uma lista explícita com dry-run por padrão. Um terapeuta rebaixado por engano é restaurado no próximo login pelo auto_subscribe_pro.

Testes vinculados

  • spec/use_cases/subscriptions/pro/validate_eligibility_spec.rb
  • spec/use_cases/subscriptions/pro/expire_when_ineligible_spec.rb

Referências