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 —, e Membership::FutureApprove só 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:

  1. A voltou para o fluxo de emissão;
  2. B foi marcada como renovação automática;
  3. como a aprovação foi em 31/08, A entrou na lista de emissão naquele dia;
  4. 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_membership passou a sair sem escrever quando a filiação já tem admin_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::FutureApprove chamava manual_approve!, que zerava a aprovação de B e a devolvia para not_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 em processing nã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_card não passa approver para Membership::FutureApprove (app/services/admin_memberships_service.rb:96). Decisão explícita: não existe cenário em que a execução chegue ali com issued_definitive_card diferente de true — a flag é gravada no próprio método, logo antes, e DATE_RELEASE_NEW_CITRG (2026-01-28) já passou. O ramo manual, que é o único que usa o approver, 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 por manual_approve! approves the membership and sends the card to the emission queue, manual_approve! attaches the profile picture to the card e manual_approve! does nothing when there is no approver.
  • test/models/membership/future_approve_test.rb — keeps the next membership in the manual flow... vira leaves the next membership untouched when the current one has not finished its flow (o documentation_status original é preservado, não mais reescrito para pending); entram approves 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 approval e returns 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... vira scenario B directs the approval to membership B, which still holds the pending flow; entram scenario B puts membership B in the emission queue and keeps A out of it, verificando Membership.with_profile_approved_card_processing, e o par de testes do email — scenario A sends the approval email for membership A e scenario B sends the approval email for membership B, not for A, ambos com assert_enqueued_email_with sobre os params do 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:

  1. 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 fica auto_issued com a data/hora da emissão de A.
  2. 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.