Fix: aprovação idempotente da filiação e auto-aprovação restrita à carteira definitiva

TLDR: AdminMembershipsService#admin_approve_membership passa a não fazer nada quando a filiação já foi aprovada, e Membership::FutureApprove passa a auto-aprovar a filiação seguinte somente para quem já tem carteira definitiva — quem não tem fica com a filiação seguinte no fluxo manual. Exige dois métodos novos em Membership: approved? e manual_approve!. E admin_issue_card passa a chamar Membership::FutureApprove no fim do método, porque é lá que issued_definitive_card vira true — sem isso a filiação seguinte, deixada no fluxo manual, nunca seria re-avaliada.


Contexto

Esta spec cobre três pontos do mesmo fluxo de aprovação. Os Pontos 1 e 2 foram implementados no commit 1ab2aff; o Ponto 3, acrescentado depois, fecha o furo que os dois primeiros abriram.

Ponto 1 — aprovação da filiação não é idempotente

admin_approve_membership (app/services/admin_memberships_service.rb:32-39) aplica a aprovação sem verificar em que ponto do fluxo a filiação está. Ele reanexa card_picture, força documentation_status = :ok, coloca card_status = :processing e sobrescreve admin_approved_at/admin_approved_by_id.

Quando o alvo é uma filiação que já foi aprovada antes, isso reescreve o registro da aprovação anterior — que não tem auditoria e portanto é perdido — e devolve a filiação para a fila de emissão. Se ela já tinha carteira emitida, o estado resultante é contraditório: card_issued_at preenchido com card_status = processing.

Ponto 2 — auto-aprovação da filiação seguinte ignora a carteira definitiva

Membership::FutureApprove (app/models/membership/future_approve.rb) é chamado no fim de admin_approve_user_profile (app/services/admin_user_profiles_service.rb:26). Hoje ele auto-aprova a filiação seguinte — automatic_approve!, que grava card_status: auto_issued, admin_approved_* e card_issued_* com o usuário “Automático” — para qualquer usuário, sem consultar issued_definitive_card.

Isso contraria R-004, que restringe a auto-aprovação de renovação a quem já tem carteira definitiva emitida. Pelo webhook (app/services/webhook_membership_service.rb:87-102) a regra é respeitada: sem a flag, a nova filiação nasce pendente e o sistema pede documentos. Pela aprovação manual de documentos, não — a filiação seguinte é auto-aprovada e ganha auto_issued mesmo para quem nunca recebeu carteira definitiva.

Efeito colateral concreto de auto_issued indevido: a fila de emissão só considera filiações em processing (app/models/membership.rb:95), então uma filiação marcada como carteira automática não aparece para ninguém emitir. Para o sistema ela tem carteira; a pessoa não tem nada em mãos, e não existe tela onde isso seja percebido.

Ponto 3 — nada re-avalia a filiação seguinte depois que a flag vira true

A ordem dos eventos na primeira filiação é fixa:

  1. Atendente aprova os documentos → admin_approve_user_profile aprova a filiação vigente e chama Membership::FutureApprove (app/services/admin_user_profiles_service.rb:26). Nesse instante issued_definitive_card ainda é false — a flag só é escrita na emissão. Com o Ponto 2 em vigor, a filiação seguinte cai no ramo manual e fica not_issued / documentation_status: pending, sem aprovação.
  2. Atendente emite a carteira → admin_issue_card grava user.update(issued_definitive_card: true) (app/services/admin_memberships_service.rb:81-84).
  3. Nada re-avalia a filiação seguinte. Ela permanece pendente indefinidamente.

Quando essa filiação entra em vigência, apolo_access_permitted? (app/models/membership.rb:200-208) exige admin_approved_by_id presente ou carteira emitida — nenhum dos dois é verdade. O membro perde acesso ao Apolo e volta a ver “documentação pendente”, que é justamente o que a spec 20260709122104_fix_status_after_definitive_card_renewal.md corrigiu. E como a fila de emissão só considera filiações em processing, ela também não aparece para ninguém aprovar ou emitir.

issued_definitive_card é escrita em um único lugar: admin_issue_card. É o único momento em que a condição consultada por Membership::FutureApprove muda de valor, e por isso é onde a re-avaliação tem que acontecer.

Os guards do use case sobrevivem ao manual_approve! — ele deixa a filiação seguinte com admin_approved_at nulo e not_issued —, então a segunda chamada passa pelo guard e entra no ramo automático. Nenhuma alteração é necessária em Membership::FutureApprove.

Métodos que precisam ser criados

O diff atual chama dois métodos que não existem em Membership e levantariam NoMethodError:

  • membership.approved? — não existe. Membership tem paid?, suspended?, is_valid?, is_renewal?, automatic_approve!; nenhum predicado de aprovação, nem via enum (documentation_status tem ok, card_status tem issued).
  • membership.manual_approve! — existe apenas em UserProfile (app/models/user_profile.rb:136-141), onde zera a aprovação e marca approval_type = :manual. Membership não tem approval_type nem método equivalente.

Ambos entram no escopo desta spec.

Nota sobre o diff atual

A alteração em app/models/user.rb:19 — o nome da coluna issued_definitive_card substituído por uma URL no bloco de annotation — é colagem acidental, não faz parte desta mudança e deve ser desfeita antes do commit.


Objetivos

  • Membership#approved? responde se a filiação já recebeu aprovação de documentação.
  • admin_approve_membership retorna sem nenhum efeito quando a filiação já está aprovada: nada atribuído, card_picture não reanexada, nada salvo.
  • O parâmetro de admin_approve_membership passa a se chamar membership em vez de resource, deixando explícito o que o método recebe.
  • Membership#manual_approve! coloca a filiação, de forma idempotente, no estado “pendente de aprovação manual”.
  • Membership::FutureApprove auto-aprova a filiação seguinte somente quando user.issued_definitive_card?; caso contrário aplica manual_approve! nela.
  • O e-mail de renovação aprovada é enviado somente no ramo automático — no ramo manual nenhum e-mail é disparado por este use case.
  • Nenhuma mudança de assinatura pública: admin_approve_membership(membership, user) e Membership::FutureApprove.call(user:, current_membership:) continuam sendo chamados como hoje.
  • admin_issue_card chama Membership::FutureApprove no fim do método, depois de a flag e o estado da filiação estarem persistidos — é o que fecha o Ponto 3.
  • A chamada na emissão é idempotente: reemissão, reenvio e reimpressão não reprocessam a filiação seguinte nem reenviam e-mail, porque o guard do use case bloqueia uma filiação já aprovada ou já emitida.
  • A chamada na emissão não auto-aprova a filiação vigente por engano: o argumento current_membership vem de user.current_membership, nunca do resource recebido.

Fora de escopo

  • Escolha da filiação que recebe a aprovação (User#current_membership em app/services/admin_user_profiles_service.rb:18) — não é alterada aqui. Consequência: quando o alvo é a filiação vigente já aprovada, o novo return faz a aprovação não produzir efeito nenhum sobre filiação alguma (ver Riscos).
  • Fila de emissão (Membership.with_profile_approved_card_processing, app/models/membership.rb:95) — mantém o filtro por vencimento futuro e a ordenação atual.
  • Escrita de issued_definitive_card — segue sendo feita apenas em admin_issue_card (app/services/admin_memberships_service.rb:81-84), quando a filiação entra em remessa, com a mesma condição de data. O que muda no método é só o acréscimo da chamada ao use case no fim.
  • Membership::FutureApprove — nenhuma linha muda: guards, ramo manual, ramo automático e e-mail ficam como estão. O Ponto 3 se resolve inteiramente no chamador.
  • Múltiplas filiações futuras pendentes (B, C…) — User#next_membership devolve só a primeira, então só ela é re-avaliada na emissão. Requisito já registrado na spec 20260811100000_multiple_pending_memberships_approval.md, que não é implementada aqui.
  • Correção dos dados já corrompidos por aprovações anteriores — tarefa separada.
  • Alternativa com raise para o caso de filiação já emitida — descrita na spec 20260904175409_guard_approval_against_issued_card.md, que não é implementada aqui. Esta spec adota o return silencioso.
  • Annotation de app/models/user.rb — reverter, não é mudança desta spec.

Mudanças

app/models/membership.rb

1. approved?, junto dos outros predicados de estado (perto de suspended?, linha 267):

ruby # Whether this membership already went through documentation approval. def approved? admin_approved_at.present? end

Critério: presença de admin_approved_at. É o mesmo critério que o resto do fluxo já usa para decidir se a filiação foi aprovada — Membership::FutureApprove checa admin_approved_at.nil? no guard. Não usar admin_approved_by_id em conjunto: o scope admin_approved (linha 101) parece exigir os dois, mas where.not(a: nil, b: nil) no Rails gera NOT (a IS NULL AND b IS NULL), ou seja “pelo menos um preenchido” — comportamento diferente do que o nome sugere e que não deve ser replicado aqui.

2. manual_approve!, junto de automatic_approve! (linha 279), como espelho do método de UserProfile:

ruby # Puts the membership back in the manual approval flow: no approval recorded, # no card issued, documentation pending. Idempotent. def manual_approve! update!( card_status: :not_issued, documentation_status: :pending, admin_approved_at: nil, admin_approved_by: nil ) end

O nome segue UserProfile#manual_approve!, que também não aprova nada — ele devolve o registro ao estado de espera por aprovação manual. Vale a mesma ressalva de nomenclatura registrada em R-004.

Observação sobre o efeito prático: quando chamado por Membership::FutureApprove, o guard do use case já garante admin_approved_at.nil? e not_issued?, então o método é um no-op na maioria das chamadas — documentation_status é o único campo que pode efetivamente mudar. Ele existe para declarar a intenção de forma explícita e para continuar correto caso o guard mude.

app/services/admin_memberships_service.rb

```ruby def admin_approve_membership(membership, user) return if membership.approved?

membership.card_picture.attach(membership.user_profile.membership_picture_file.blob)
membership.documentation_status = :ok
membership.card_status = :processing
membership.admin_approved_at = Time.now
membership.admin_approved_by_id = user.id
membership.save!   end ```
  • return sem valor: o método já não tinha retorno significativo, e o único chamador de produção (app/services/admin_user_profiles_service.rb:22) ignora o retorno.
  • Renomear resource → membership é interno ao método; a chamada é posicional e não muda.

app/models/membership/future_approve.rb

```ruby def call! next_membership = user.next_membership return Success() unless next_membership && next_membership != current_membership && next_membership.admin_approved_at.nil? && next_membership.not_issued?

unless user.issued_definitive_card?
  next_membership.manual_approve!
  return Success()
end

next_membership.automatic_approve!

Success()   end ```
  • O guard existente é preservado integralmente.
  • O use case envia e-mail apenas no ramo automático — o envio foi removido pelo adendo de 2026-09-08 e restaurado pelo adendo de 2026-09-09. No diff original ele estava depois do if/else, o que faria quem não tem carteira definitiva receber “Sua filiação ao CITRG foi renovada com sucesso” sem que a filiação tivesse sido aprovada; a correção foi movê-lo para dentro do ramo automático, que é onde ele está hoje.
  • Nenhum e-mail é enviado no ramo manual. As duas opções descartadas: ApplicationMailer#request_documents_for_membership_email (existe, usado pelo webhook em webhook_membership_service.rb:43-48) seria contraditório logo após uma aprovação bem-sucedida; e o e-mail de renovação aprovada seria factualmente falso. Se o negócio quiser comunicar essa situação, é um e-mail novo, fora desta spec.
  • issued_definitive_card? — o predicado booleano do ActiveRecord para a coluna.

app/services/admin_memberships_service.rb — admin_issue_card

Acrescentar a chamada no fim do método, depois do bloco first_time, e reaproveitar o user que já é resolvido para escrever a flag:

```ruby shipment_membership.save!

user = resource.user

# ... resolução de date_release_new_citrg

if Date.current >= date_release_new_citrg
  user.update(issued_definitive_card: true)
end

if shipment_kind == "first_time"
  resource.card_status = :issued
  resource.card_issued_at = Time.now
  resource.card_issued_by_id = admin_user.id
  resource.save!
end

# The definitive card flag may have just been granted above, and it is the
# condition Membership::FutureApprove checks. Re-evaluate the next
# membership here: the documentation approval ran before the flag existed
# and left it in the manual flow, with nothing else to pick it up.
#
# current_membership must come from the user, not from resource: when an old
# membership is reissued, User#next_membership falls back to the membership
# in force, and only this argument keeps the use case guard from approving it.
Membership::FutureApprove.call(user: user, current_membership: user.current_membership)   end ```

Dois detalhes que não são livres de escolha:

1. current_membership: user.current_membership, não resource. User#next_membership (app/models/user.rb:92-99) tem um fallback: quando não existe filiação posterior, ele retorna a própria vigente. O guard next_membership != current_membership é o que neutraliza esse fallback — e só neutraliza se o argumento for a mesma filiação que current_membership resolve.

Passar resource quebraria isso numa reimpressão: emitindo de novo a carteira de uma filiação vencida (resource = filiação antiga), next_membership devolveria a vigente pendente, o guard veria vigente != antiga e o use case auto-aprovaria a filiação vigente sem nenhuma análise de documento. Passar user.current_membership reproduz exatamente a forma como admin_approve_user_profile já chama o use case e mantém o fallback inofensivo.

user.current_membership pode ser nil (usuário sem filiação paga); o guard next_membership && cobre esse caso com Success() sem efeito.

2. Fora de transação, no fim do método. admin_issue_card não abre transação e a emissão já está persistida quando a chamada acontece. Se o use case falhar, a carteira continua emitida e na remessa — o que é o desejado; a consequência de UI está em Riscos.

Nenhuma mudança de assinatura: admin_issue_card(resource, admin_user, shipment_details) continua igual, e o member_action :issue_card (app/admin/memberships.rb:248) não muda.


Como verificar

bash make run.test path=test/models/membership_test.rb make run.test path=test/models/membership/future_approve_test.rb make run.test path=test/services/admin_memberships_service_test.rb make run.test path=test/services/admin_user_profiles_service_test.rb bundle exec rubocop app/models/membership.rb app/models/membership/future_approve.rb app/services/admin_memberships_service.rb

Casos novos

Membership#approved?

Cenário Esperado
admin_approved_at preenchido true
admin_approved_at nulo false
admin_approved_at nulo e admin_approved_by_id preenchido false — o critério é só a data

Membership#manual_approve!

Cenário Esperado
Filiação aprovada e auto_issued Após a chamada: not_issued, documentation_status == "pending", admin_approved_at e admin_approved_by_id nulos
Filiação já pendente e not_issued Nada muda; não levanta erro (idempotente)

AdminMembershipsService#admin_approve_membership

Cenário Esperado
Filiação já aprovada (admin_approved_at preenchido) Retorna sem efeito: card_status, documentation_status, admin_approved_at, admin_approved_by_id e card_issued_at intactos após reload; card_picture não anexada
Filiação sem aprovação Aprova: processing, documentation_status == "ok", admin_approved_at presente, admin_approved_by_id == user.id, card_picture anexada

Membership::FutureApprove

Cenário Esperado
Usuário com issued_definitive_card = true, filiação seguinte pendente e not_issued auto_issued, admin_approved_* e card_issued_* preenchidos com o usuário “Automático”; membership_renewal_approved_email enfileirado (ver adendo de 2026-09-09)
Usuário com issued_definitive_card = false, mesma situação Filiação seguinte permanece not_issued, sem aprovação, documentation_status == "pending"; nenhum e-mail enfileirado
Sem filiação seguinte, ou seguinte igual à recebida, ou já aprovada, ou já emitida Success() sem efeito, com e sem a flag — guards preservados

AdminMembershipsService#admin_issue_card

Todos com ENV["DATE_RELEASE_NEW_CITRG"] fixado no teste — anterior a hoje para conceder a carteira definitiva, posterior para não conceder.

Cenário Esperado
Filiação vigente sendo emitida (first_time) e filiação seguinte paga, pendente e not_issued Seguinte fica auto_issued, admin_approved_* e card_issued_* preenchidos com o usuário “Automático”; membership_renewal_approved_email enfileirado (ver adendo de 2026-09-09)
Mesma emissão repetida (reprint na sequência) Seguinte permanece como estava; nenhum e-mail novo enfileirado
Emissão (reprint) sobre filiação vencida, com a vigente pendente e not_issued e sem filiação futura A vigente não é aprovada: continua not_issued, sem admin_approved_at, documentation_status intacto. É o teste que trava a escolha do argumento
Data anterior a DATE_RELEASE_NEW_CITRG issued_definitive_card continua false; a seguinte permanece not_issued sem aprovação; nenhum e-mail

Testes existentes que vão quebrar

Nenhuma fixture de usuário define issued_definitive_card (test/fixtures/users.yml), então todas valem false por default. Estes dois testes de test/services/admin_user_profiles_service_test.rb esperam a auto-aprovação atual e passam a falhar:

Teste Linha O que fazer
“admin_approve_user_profile auto-approves the next membership (B) when pending and not_issued” 70 Adicionar @user.update!(issued_definitive_card: true) no arranjo, mantendo a asserção — o teste passa a cobrir o ramo automático
“admin_approve_user_profile sends two emails when B is auto-approved” 146 Adicionar a flag; o nome e a asserção de dois e-mails permanecem (ver adendo de 2026-09-09)

Adicionar, em contrapartida, os dois casos espelhados sem a flag: filiação seguinte permanece pendente e apenas um e-mail é enviado (o da filiação aprovada).

Continuam passando sem alteração: linha 95 (“does not auto-approve B when B already has admin_approved_at”), linha 121 (“does not auto-approve B when card is not not_issued”), linha 168 (“sends one email when there is no next membership”) e as linhas 22 (falha antes de chegar ao serviço), 31 e 50 (fazem stub/mock de admin_approve_membership).

Cenário manual (console)

```ruby user = User.find_by(email: “…”) user.issued_definitive_card user.memberships.order(:valid_since).map { |m| [m.register_number, m.valid_until, m.card_status, m.admin_approved_at] }

aprovar documentos pelo admin e conferir

user.memberships.reload.map { |m| [m.card_status, m.admin_approved_at, m.documentation_status] } ```

Esperado sem a flag: a filiação seguinte segue not_issued, sem aprovação, documentation_status pendente. Com a flag: auto_issued e aprovação do usuário “Automático”.


Riscos

Risco Impacto Observação
Falha do use case depois da carteira emitida member_action :issue_card (app/admin/memberships.rb:262) tem rescue => e que renderiza “Erro ao emitir carteira” mesmo com a carteira já emitida e na remessa. O atendente vê erro numa operação que deu certo Aceito: o caminho de falha é update! na filiação seguinte e deliver_later, ambos improváveis. Isolar o erro (log em vez de exceção) é decisão separada
Auto-aprovação da filiação seguinte sem análise de documento É R-004 sendo aplicada, mas agora ela alcança a primeira filiação, não só a renovação via webhook: quem recebeu carteira definitiva tem a filiação seguinte já paga aprovada sem reenviar documento Comportamento pretendido, registrado em R-007
Reimpressão/reenvio como gatilho Qualquer emissão, de qualquer shipment_kind, re-avalia a filiação seguinte Consequência aceita: se o usuário tem carteira definitiva, a filiação seguinte deveria estar aprovada de qualquer forma. Os guards impedem retrabalho e e-mail duplicado
Segunda filiação futura pendente (C) next_membership devolve só B; C fica pendente sem ninguém para processar — o mesmo furo um nível acima Fora de escopo aqui; é o objeto da spec 20260811100000_multiple_pending_memberships_approval.md
Conflito com o Cenário A do relatório do negócio O relatório pede que, aprovada a filiação vigente pendente, a renovação “entre no fluxo de renovação automática”. Com esta mudança isso deixa de acontecer para quem não tem carteira definitiva Com o Ponto 3 o cenário volta a funcionar assim que a carteira é emitida. A divergência sobrevive só para quem nunca recebe carteira definitiva — precisa de confirmação de quem definiu a regra
return silencioso em admin_approve_membership Quando o alvo é uma filiação já aprovada, a tela exibe “Perfil aprovado!”, o e-mail é enviado e nenhuma filiação avança. O atendente não tem como perceber A alternativa com raise está em 20260904175409_guard_approval_against_issued_card.md
Reenvio de documentos sobre filiação já aprovada A foto nova não é reanexada, porque o método retorna antes. A carteira sai com a foto antiga Hoje esse caminho reanexa a foto. Confirmar se é aceitável
manual_approve! é destrutivo por natureza Chamado fora do guard do FutureApprove, apaga aprovação e emissão de uma filiação Só é chamado a partir do use case; manter assim

Documentação

  • Atualizar .project/docs/rules/membership/definitive_card_renewal_auto_approval.md (R-004): acrescentar que issued_definitive_card é escrita apenas em admin_issue_card, quando a filiação entra em remessa, que a auto-aprovação da filiação seguinte pela aprovação manual de documentos passa a respeitar a mesma flag, e o ponteiro para R-007.
  • Criar .project/docs/rules/membership/next_membership_reevaluated_on_card_issue.md (R-007): a filiação seguinte é re-avaliada na emissão da carteira; a aprovação de documentos, isolada, não basta.
  • Criar .project/docs/rules/membership/membership_approval_is_idempotent.md (R-008): aprovação de filiação já aprovada não produz efeito.
  • Criar .project/docs/rules/membership/next_membership_auto_approval_requires_definitive_card.md (R-009): a filiação seguinte só é auto-aprovada para quem tem carteira definitiva; sem ela, permanece no fluxo manual.
  • Adicionar as três na tabela de .project/docs/README.md e em RULES.md (última antes desta spec: R-006). Se a spec 20260904175409 for implementada antes, renumerar.

Adendo (2026-09-08) — o e-mail sai do FutureApprove

Superado pelo adendo de 2026-09-09 — a remoção descrita abaixo foi revertida. O registro fica aqui pelo histórico da decisão.

Aviso: Membership::FutureApprove deixou de enviar membership_renewal_approved_email. O envio foi removido depois do teste manual em staging, que mostrou o membro recebendo dois e-mails idênticos pela mesma renovação.

O que foi observado

Teste manual em citrg-api--staging (release v334, commit 8cd54370), usuário #19855 sem carteira definitiva, filiação vigente #24813 e seguinte #24814:

Momento Origem membership_id
18:59:40 UTC — aprovação dos documentos AdminUserProfilesService#fire_approved_user_profile_email 24813
19:00:26 UTC — emissão da carteira Membership::FutureApprove 24814

Os dois e-mails são iguais byte a byte: mesmo assunto (“Sua filiação ao CITRG foi renovada com sucesso”) e um corpo que renderiza apenas @user.name e @membership.register_number_pad_dot (app/views/membership_mailer/membership_renewal_approved_email.html.erb) — e as duas filiações do mesmo membro compartilham o register_number. O segundo e-mail não carrega nenhuma informação nova.

Por que a remoção é no FutureApprove, e não em outro remetente

membership_renewal_approved_email tem três remetentes:

Remetente Evento comunicado
AdminUserProfilesService#fire_approved_user_profile_email os documentos foram aprovados
WebhookMembershipService#send_membership_renewal_approved_email o pagamento da renovação foi confirmado
Membership::FutureApprove aprovação antecipada do período seguinte — passo interno

Sempre que o FutureApprove enviaria, o membro já recebeu um e-mail idêntico: para existir filiação seguinte distinta, o usuário tem duas ou mais filiações, logo is_renewal? é true e a aprovação de documentos já disparou o mesmo e-mail (app/services/admin_user_profiles_service.rb:30-38). Não existe caminho em que o envio do use case seja a única notificação da renovação. Os outros dois remetentes são o único aviso dos seus respectivos eventos e permanecem intactos.

Isso resolve as duas formas da duplicidade:

  • usuário que já tem carteira definitiva na moderação — dois e-mails no mesmo segundo;
  • usuário sem carteira definitiva — um na moderação e outro na emissão, dias depois, pelo caminho do R-007.

admin_issue_card continua sem enviar e-mail nenhum, como antes desta spec: a comunicação da carteira é a ação manual Enviar atualização de carteira (member_action :send_card_status_update_email), que depende do card_tracking_code.

Alternativa descartada

Manter o envio com um template próprio (“o período de tal a tal foi aprovado antecipadamente”, com valid_since/valid_until). Deixaria de ser duplicidade e passaria a ser informação nova, mas exige template e copy novos, e não foi pedido por quem definiu a regra. Se o negócio quiser essa comunicação, é uma spec separada.

Efeitos

  • app/models/membership/future_approve.rb: bloco do MembershipMailer removido. O motivo fica registrado aqui e no R-009, não em comentário no código.
  • test/models/membership/future_approve_test.rb: “sends the renewal approved email when the user has a definitive card” virou “sends no email when the user has a definitive card” (assert_no_enqueued_emails).
  • test/services/admin_memberships_service_test.rb: “…sends the renewal approved email for the auto-approved next membership” virou “…sends no email when auto-approving the next membership”.
  • test/services/admin_user_profiles_service_test.rb: “sends two emails when B is auto-approved” virou “sends one email when B is auto-approved”.
  • R-004 e R-007 atualizados: a auto-aprovação da filiação seguinte pelos caminhos do admin não notifica; o e-mail de renovação aprovada do webhook não muda.

Adendo (2026-09-09) — o e-mail volta ao FutureApprove

Aviso: a remoção descrita no adendo de 2026-09-08 foi revertida (git revert do commit 3356f03). Membership::FutureApprove volta a enviar membership_renewal_approved_email para a filiação seguinte, e os três testes voltaram ao texto e às asserções originais.

O que estava errado na análise anterior

O adendo de 2026-09-08 afirma: “Não existe caminho em que o envio do use case seja a única notificação da renovação.” Isso não se sustenta. Os dois chamadores do FutureApprove se comportam de forma oposta:

Chamador Ação no Admin O chamador envia e-mail? Total da ação
AdminUserProfilesService#admin_approve_user_profile “Moderar Documentos” sim, sempre (fire_approved_user_profile_email) 2 — duplicidade garantida
AdminMembershipsService#admin_issue_card “Emitir carteira” não, nenhum 1 — só o do FutureApprove

O member_action :issue_card (app/admin/memberships.rb:245) não dispara e-mail: o send_card_status_update_email é um botão separado, acionado à mão, e só aparece quando existe card_tracking_code. Nessa ação, o envio do use case era o único e-mail.

O caso de staging que motivou a remoção (18:59:40 e 19:00:26) não é duplicidade de uma mesma ação: são duas ações distintas do atendente — moderar e emitir — com 46 segundos entre elas. Pareceu duplicata porque o conteúdo é idêntico e chegou colado.

E existe um cenário em que a remoção deixou o membro sem aviso nenhum:

  1. os documentos da filiação vigente são aprovados quando ela ainda é a única — is_renewal? é false, o membro recebe user_profile_documentation_approved_email;
  2. o webhook cria a filiação seguinte com a flag ainda false — o membro recebe request_documents_for_membership_email;
  3. a carteira é emitida, a flag vira true, o FutureApprove auto-aprova a filiação seguinte — e nada é enviado.

O membro fica com “envie seus documentos” como última comunicação, para documentos que nunca mais serão pedidos.

Por que reverter em vez de corrigir agora

A remoção acertava o caminho da moderação e quebrava o da emissão. Voltar ao estado anterior restabelece a cobertura do caminho da emissão ao custo de reintroduzir a duplicidade conhecida na moderação — que é o comportamento que já rodava em produção antes desta spec, e não uma regressão nova.

O que fica em aberto

Resolvido pelo adendo de 2026-09-09 — a vigência no corpo do e-mail, com escopo menor que o descrito abaixo.

A duplicidade da moderação continua real e determinística. A correção pretendida é a “alternativa descartada” do adendo anterior, agora escolhida: template e mailer próprios para a aprovação antecipada da filiação seguinte, com valid_since/valid_until no corpo.

Isso é o que quebra a identidade entre os dois e-mails. Hoje o único parâmetro que difere entre eles é o membership_id, e ele não aparece em lugar nenhum: o corpo renderiza apenas @user.name e register_number_pad_dot — e WebhookMembershipService#create_or_return_register_number reusa o register_number do usuário, então filiação vigente e seguinte compartilham o número. Nem o metadata distingue, porque grava @user.id, não o id da filiação. A vigência é o único dado que varia.

Fica para uma spec separada, que precisa definir a copy do e-mail novo. Observação para essa spec: membership_renewal_approved_email.text.erb está com 0 bytes — o template novo deve nascer com as duas versões preenchidas.

Adendo (2026-09-09) — a vigência no corpo do e-mail

Aviso: o item “em aberto” acima foi resolvido, mas sem mailer novo. membership_renewal_approved_email passou a renderizar a vigência da filiação no corpo, e o template é o único arquivo alterado.

A mudança

app/views/membership_mailer/membership_renewal_approved_email.html.erb, um parágrafo novo depois da confirmação da renovação:

```erb

O período renovado tem vigência de <%= @membership.valid_since.strftime("%d/%m/%Y") %> a <%= @membership.valid_until.strftime("%d/%m/%Y") %>.

```

O formato segue o precedente do repo — membership_first_day_of_month_reminder_email.html.erb:3 e membership_expired_reminder_email.html.erb:3 já usam valid_until.strftime("%d/%m/%Y").

valid_since e valid_until são NOT NULL no schema, então não existe caminho em que o parágrafo renderize vazio.

Por que no template existente, e não em um mailer novo

O adendo anterior propôs template e copy próprios para a aprovação antecipada. A vigência no template existente resolve o mesmo problema com um parágrafo e alcança os três remetentes de uma vez, não só o do FutureApprove:

Remetente Filiação no e-mail O que a vigência acrescenta
AdminUserProfilesService#fire_approved_user_profile_email a vigente o período que acabou de ser aprovado
WebhookMembershipService#send_membership_renewal_approved_email a que o pagamento criou o período que o pagamento comprou — hoje o membro não vê quando a renovação começa
Membership::FutureApprove a seguinte o período seguinte, distinto do da vigente

Na moderação os dois e-mails continuam sendo enviados, mas deixam de ser idênticos: a vigência é o único dado que varia entre as duas filiações, porque o register_number é compartilhado.

Fora deste adendo

  • Assunto — continua “Sua filiação ao CITRG foi renovada com sucesso” nos três remetentes. Consequência aceita: na caixa de entrada os dois e-mails da moderação seguem parecendo o mesmo; a distinção só aparece ao abrir.
  • Número de envios — nenhum remetente foi removido nem acrescentado; a duplicidade da moderação permanece, agora com conteúdos distintos.
  • metadata do mailer — continua gravando metadata["object"] = "User" e @user.id, então o Postmark ainda não distingue qual filiação gerou cada envio.
  • membership_renewal_approved_email.text.erb — segue com 0 bytes. Correção da observação do adendo anterior: os quatro .text.erb do membership_mailer estão zerados, não só este — é o padrão atual do repo, e não uma pendência criada por esta spec.