Fix: a aprovação vai para a filiação que ainda não concluiu o fluxo
TLDR:
Membership#manual_approve!deixa de ser um reset e passa a aprovar de fato a filiação seguinte —card_status: processing,documentation_status: ok, foto anexada e o admin como aprovador —, eMembership::FutureApprovesó a executa quando a filiação vigente já concluiu o fluxo (aprovada e com carteira emitida), devolvendo qual filiação aprovou para que o email de aprovação aponte para ela. Com isso a aprovação documental deixa de ficar presa na filiação vigente e passa a cair na filiação que ainda tem o fluxo pendente.
Contexto
O caso relatado
Uma terapeuta com duas filiações consecutivas:
- Filiação A: agosto/2025 a agosto/2026 — criada em 21/08/2025, documentação aprovada em 08/09/2025, carteira emitida em 11/09/2025 na remessa 186. Fluxo concluído.
- Filiação B: agosto/2026 a agosto/2027 — criada na renovação de 07/08/2026, documentação aprovada em 31/08/2026 depois de recusas.
A aprovação de 31/08 deveria ter caído em B. Caiu em A, que ainda estava vigente naquele dia apesar de já aprovada e emitida. Consequências em cadeia:
- A voltou para o fluxo de emissão;
- B foi marcada como renovação automática;
- como a aprovação foi em 31/08, A entrou na lista de emissão naquele dia;
- em 01/09, ao encerrar a vigência, A saiu da fila automaticamente — antes de a emissão acontecer.
O que já tinha sido feito e por que não bastou
Três correções anteriores tocaram este mesmo fluxo:
- R-008 (
e9d700f) —admin_approve_membershippassou a sair sem escrever quando a filiação já temadmin_approved_at. Isso impede a regressão de A, mas não redireciona a aprovação para B: o resultado passou a ser “nada acontece”. - R-009 (
e9d700f) — a auto-aprovação da filiação seguinte ficou restrita a quem tem carteira definitiva. Sem a flag,Membership::FutureApprovechamavamanual_approve!, que zerava a aprovação de B e a devolvia paranot_issued/pending. Ou seja: no cenário em que a aprovação deveria ir para B, ela ia para lugar nenhum. - R-010 (
fc440f5) — a fila passou a priorizar quem vence no mês corrente. A ordenação resolve posição na fila, não destino da aprovação: uma filiação que nunca entrou emprocessingnão é ordenada por critério nenhum.
O buraco que restou é o direcionamento: o sistema escolhe a filiação pelo critério “é a vigente”, sem perguntar se ela ainda tem fluxo pendente.
Os dois cenários que precisam funcionar
Cenário A — filiação vigente sem aprovação e sem emissão. A vence em setembro/2026 e continua sem aprovação e sem emissão; a renovação em setembro cria B; a documentação é aprovada ainda em setembro, com A vigente.
→ A aprovação vai para A, que é quem tem o fluxo pendente. B fica intocada e só entra na renovação automática depois da emissão manual de A, herdando a data/hora dessa emissão. Na fila, A é priorizada pelo vencimento no mês, independentemente da data de aprovação.
Cenário B — filiação vigente já aprovada e emitida. A vence em setembro/2026 mas já tem aprovação e carteira; a renovação em setembro cria B; a documentação é aprovada ainda em setembro, com A vigente.
→ A aprovação vai para B. A permanece intocada. B entra na fila de emissão pela ordenação normal por data de aprovação.
Objetivos
- A aprovação documental cai na filiação que ainda não concluiu o fluxo, e não na que apenas está vigente.
- Cenário A: A é aprovada e entra na fila; B espera a emissão de A.
- Cenário B: A não é tocada e B é aprovada, entrando na fila de emissão.
- Nenhuma filiação com carteira já emitida volta para a fila.
- O email de aprovação identifica a filiação que foi aprovada, não a que apenas está vigente.
Fora de escopo
admin_issue_cardnão passaapproverparaMembership::FutureApprove(app/services/admin_memberships_service.rb:96). Decisão explícita: não existe cenário em que a execução chegue ali comissued_definitive_carddiferente detrue— a flag é gravada no próprio método, logo antes, eDATE_RELEASE_NEW_CITRG(2026-01-28) já passou. O ramo manual, que é o único que usa oapprover, não é alcançado por esse chamador.- Auditoria/histórico de aprovações.
Mudanças
app/models/membership.rb
manual_approve! troca de semântica: era um reset (not_issued, pending, aprovação zerada), passa a ser a aprovação manual de fato.
```ruby def manual_approve!(approver) return if approver.nil?
picture = user_profile.membership_picture_file card_picture.attach(picture.blob) if picture.attached?
update!( card_status: :processing, documentation_status: :ok, admin_approved_at: Time.current, admin_approved_by: approver ) end ```
O anexo é condicional (if picture.attached?), diferente de AdminMembershipsService#admin_approve_membership, que acessa .blob direto. approver nulo é um no-op — a filiação não é aprovada sem responsável.
app/models/membership/future_approve.rb
Recebe approver e só chama manual_approve! quando a filiação vigente já concluiu o fluxo:
```ruby attributes :user, :current_membership, :approver
unless user.issued_definitive_card? if approver && current_membership_approved_and_issued? next_membership.manual_approve!(approver)
return Success(result: { approved_membership: next_membership }) end
return Success() end
def current_membership_approved_and_issued? current_membership.approved? && Membership::ISSUED_CARD_STATUSES.include?(current_membership.card_status.to_sym) end ```
O guard de entrada existente é preservado (next_membership distinta, sem admin_approved_at e not_issued?). No cenário A a vigente está em processing — fora de ISSUED_CARD_STATUSES —, então B não é tocada.
approved_membership só volta preenchido no ramo manual em que a filiação seguinte foi de fato aprovada. Todos os demais ramos continuam devolvendo Success() vazio: é o que mantém o email inalterado no cenário A, quando não existe next_membership e no ramo automático — que já dispara o próprio email de renovação.
app/services/admin_user_profiles_service.rb
Repassa o admin que aprovou e usa o retorno do caso de uso para escolher o destinatário do email:
```ruby result = Membership::FutureApprove.call( user: @profile_user, current_membership: @membership, approver: user )
fire_approved_user_profile_email(result[:approved_membership] || @membership) ```
fire_approved_user_profile_email passa a receber a filiação por parâmetro em vez de ler @membership. Antes, o email era disparado antes do Membership::FutureApprove e sempre com a filiação vigente: no cenário B ele saía apontando para A, que não foi aprovada — membership_id (ou register_number) errados. O template não mudava, porque is_renewal? é user.memberships.count > 1 e vale para as duas.
Efeito colateral aceito: o email passa a ser enfileirado depois do Membership::FutureApprove. Se ele levantar exceção, o email não sai — antes sairia. O caso de uso roda fora da transação e é curto, então o risco é baixo.
Testes
test/models/membership_test.rb— os dois testes do reset antigo são substituídos pormanual_approve! approves the membership and sends the card to the emission queue,manual_approve! attaches the profile picture to the cardemanual_approve! does nothing when there is no approver.test/models/membership/future_approve_test.rb—keeps the next membership in the manual flow...viraleaves the next membership untouched when the current one has not finished its flow(odocumentation_statusoriginal é preservado, não mais reescrito parapending); entramapproves the next membership when the current one is already approved and issued,does not approve the next membership when no approver is given,returns the next membership as the approved one when it takes over the approvalereturns no approved membership when the current one still holds the pending flow.test/services/membership_approval_scenarios_test.rb—scenario B keeps membership B in the manual flow...virascenario B directs the approval to membership B, which still holds the pending flow; entramscenario B puts membership B in the emission queue and keeps A out of it, verificandoMembership.with_profile_approved_card_processing, e o par de testes do email —scenario A sends the approval email for membership Aescenario B sends the approval email for membership B, not for A, ambos comassert_enqueued_email_withsobre osparamsdo mailer.
Como verificar
Suíte completa:
bash
make run.test
Esperado: 396 tests, 980 assertions, 3 failures, 0 errors, 1 skips. As três falhas são pré-existentes na main e não têm relação com esta mudança (Api::V1::ApoloMembershipControllerTest#keeps_the_current_membership_on_the_last_day_of_its_validity, o equivalente em Api::V2 e Api::V2::TherapistControllerTest#should_return_success_when_membership_has_active_and_inactive_associations) — confirmado rodando os mesmos arquivos com as mudanças em git stash.
No Admin, com um membro sem carteira definitiva e duas filiações encadeadas:
- Cenário A — A vigente sem aprovação, B criada. Aprovar o perfil → A fica “Em emissão” com a foto anexada e aparece no scope “Em Emissão”; B continua
not_issued, sem aprovação. Emitir a carteira de A → B ficaauto_issuedcom a data/hora da emissão de A. - Cenário B — A vigente já aprovada e emitida, B criada. Aprovar o perfil → A permanece
issued, com a aprovação original intacta e sem nova cópia da foto; B fica “Em emissão”, aprovada pelo admin, e aparece no scope “Em Emissão”. O email de renovação sai com os dados de B.
Documentação
- R-009 reescrita: a semântica de
manual_approve!deixou de ser reset, a tabela de decisão ganhou a coluna do estado da filiação vigente e a lista de testes vinculados foi atualizada. - R-011 criada para o direcionamento em si — “a aprovação vai para a filiação que ainda não concluiu o fluxo” —, com as linhas correspondentes em README.md e RULES.md.