Fix: apolo_membership nega acesso a quem renovou mas ainda não foi reaprovado

TLDR: Api::V1::ApoloMembershipController passa a considerar a filiação paga mais recente (não só a já aprovada) e Membership#public_serialize passa a expor valid_until/valid_until_timestamp reais mesmo sem aprovação própria, evitando que o trg-club-api rebaixe quem renovou e está pago mas ainda não foi reaprovado manualmente.

Nota: esta spec foi superada em parte pela correção de renovação antecipada. A troca para .paid descrita aqui fez a filiação futura ser sempre a escolhida. A regra vigente está em R-001.

Contexto

Toda renovação (WebhookMembershipService#initialize_new_membership) cria uma nova Membership com o mesmo register_number, e essa nova linha nasce sem admin_approved_at/admin_approved_by_id a menos que user.issued_definitive_card == true — flag que só existe para quem foi emitido/aprovado depois do DATE_RELEASE_NEW_CITRG. Não há backfill dessa flag para quem já tinha carteira emitida/aprovada antes desse mecanismo existir.

Caso real (register_number 15820):

Filiação payment_status valid_until card_status admin_approved_*
antiga (id 26614) — 2026-05-31 (expirada) issued presentes
nova (id 35412) paid 2027-06-30 (válida) not_issued em branco

O endpoint GET /api/v1/apolo_membership selecionava apenas entre filiações aprovadas (@user.memberships.admin_approved.order_by_newest_valid_until.try(:first)), então pegava a 26614 (expirada) e ignorava a 35412 (paga e válida). Além disso, valid_until/valid_until_timestamp em public_serialize só eram preenchidos quando apolo_access_permitted? era true — senão vinham null.

O trg-club-api (Subscriptions::Pro::ValidateEligibility) só lê membership.dig(:valid_until_timestamp) para decidir elegibilidade PRO — nunca olha apolo_access_status. Com valid_until_timestamp: null, entendia que não havia filiação ativa e rebaixava a assinatura PRO da pessoa, mesmo com pagamento em dia e aprovação anterior. O gap era puramente de processo (falta de reaprovação manual pós-renovação para quem não tem issued_definitive_card), não inadimplência.

Decisão explícita: apolo_access_permitted? (e portanto apolo_access_status) não é alterado — continua exigindo aprovação própria ou carteira emitida, sem considerar histórico de aprovação de filiações anteriores. Esse booleano segue sendo o gate de compliance do CITRG para os outros consumidores do endpoint. O fix resolve o caso por duas mudanças mais cirúrgicas: qual filiação é selecionada, e quais campos do serializer dependem de apolo_access_permitted?.

Objetivos

  • O endpoint apolo_membership passa a considerar a filiação paga mais recente (por valid_until) como candidata, não só a já aprovada — preservando o 404 para quem nunca teve nenhuma filiação paga.
  • valid_until e valid_until_timestamp no public_serialize sempre refletem a data real da filiação selecionada, independente de apolo_access_permitted?.
  • apolo_access_status continua computado por apolo_access_permitted? sem mudanças.

Fora de escopo

  • Mudanças em WebhookMembershipService/issued_definitive_card — commits recentes na main (#201/#198/#196/#194) já corrigiram os status exibidos no dashboard/admin para quem tem a flag, mas não tocam no endpoint público nem resolvem quem nunca teve a flag setada (membros legados).
  • Mascarar suspensão ou pagamento pendente — payment_status/suspended já vêm corretos no serializer.

Mudanças

app/controllers/api/v1/apolo_membership_controller.rb

show: troca a seleção de @user.memberships.admin_approved.order_by_newest_valid_until.try(:first) para @user.memberships.paid.order_by_newest_valid_until.try(:first) — como payment_status não muda com o tempo, isso continua pegando a mais recente entre as pagas (aprovada ou não), preservando o 404 para quem nunca pagou.

app/models/membership.rb

public_serialize: valid_until/valid_until_timestamp deixam de ser condicionados a apolo_access_permitted?.

diff apolo_access_status: apolo_access_permitted?, payment_status:, register_number: register_number_pad, - valid_until: apolo_access_permitted? ? valid_until.strftime("%m/%Y") : nil, + valid_until: valid_until.strftime("%m/%Y"), card_status:, - valid_until_timestamp: apolo_access_permitted? ? valid_until : nil, + valid_until_timestamp: valid_until,

Plano de implementação

  1. test: Membership#public_serialize — valid_until/valid_until_timestamp preenchidos mesmo quando apolo_access_permitted? é false; regressão do caso já aprovado; apolo_access_status sem mudança de comportamento. Arquivo: test/models/membership_test.rb
  2. test: Api::V1::ApoloMembershipController#show — retorna a filiação paga mais recente mesmo sem aprovação própria; mantém 404 para usuário sem nenhuma filiação paga; regressão do caso já aprovado. Arquivo: test/controllers/api/v1/apolo_membership_controller_test.rb
  3. fix: trocar o scope de seleção no controller (paid em vez de admin_approved).
  4. fix: consolidar a mudança em public_serialize.

Como verificar

bash make test test=test/models/membership_test.rb make test test=test/controllers/api/v1/apolo_membership_controller_test.rb

Manual (console de produção, somente leitura):

ruby user = User.find_by(email: "...") membership = user.memberships.paid.order_by_newest_valid_until.first membership.id # deve ser 35412, não 26614 membership.public_serialize[:valid_until_timestamp] # deve ser "2027-06-30", não null membership.apolo_access_permitted? # continua false — o gate não muda

Documentação