Fix: aprovação idempotente da filiação e auto-aprovação restrita à carteira definitiva
TLDR:
AdminMembershipsService#admin_approve_membershippassa a não fazer nada quando a filiação já foi aprovada, eMembership::FutureApprovepassa 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 emMembership:approved?emanual_approve!. Eadmin_issue_cardpassa a chamarMembership::FutureApproveno fim do método, porque é lá queissued_definitive_cardviratrue— 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:
- Atendente aprova os documentos →
admin_approve_user_profileaprova a filiação vigente e chamaMembership::FutureApprove(app/services/admin_user_profiles_service.rb:26). Nesse instanteissued_definitive_cardainda éfalse— a flag só é escrita na emissão. Com o Ponto 2 em vigor, a filiação seguinte cai no ramo manual e ficanot_issued/documentation_status: pending, sem aprovação. - Atendente emite a carteira →
admin_issue_cardgravauser.update(issued_definitive_card: true)(app/services/admin_memberships_service.rb:81-84). - 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.Membershiptempaid?,suspended?,is_valid?,is_renewal?,automatic_approve!; nenhum predicado de aprovação, nem via enum (documentation_statustemok,card_statustemissued).membership.manual_approve!— existe apenas emUserProfile(app/models/user_profile.rb:136-141), onde zera a aprovação e marcaapproval_type = :manual.Membershipnão temapproval_typenem 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_membershipretorna sem nenhum efeito quando a filiação já está aprovada: nada atribuído,card_picturenão reanexada, nada salvo.- O parâmetro de
admin_approve_membershippassa a se chamarmembershipem vez deresource, 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::FutureApproveauto-aprova a filiação seguinte somente quandouser.issued_definitive_card?; caso contrário aplicamanual_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)eMembership::FutureApprove.call(user:, current_membership:)continuam sendo chamados como hoje. admin_issue_cardchamaMembership::FutureApproveno 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_membershipvem deuser.current_membership, nunca doresourcerecebido.
Fora de escopo
- Escolha da filiação que recebe a aprovação (
User#current_membershipemapp/services/admin_user_profiles_service.rb:18) — não é alterada aqui. Consequência: quando o alvo é a filiação vigente já aprovada, o novoreturnfaz 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 emadmin_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_membershipdevolve só a primeira, então só ela é re-avaliada na emissão. Requisito já registrado na spec20260811100000_multiple_pending_memberships_approval.md, que não é implementada aqui. - Correção dos dados já corrompidos por aprovações anteriores — tarefa separada.
- Alternativa com
raisepara o caso de filiação já emitida — descrita na spec20260904175409_guard_approval_against_issued_card.md, que não é implementada aqui. Esta spec adota oreturnsilencioso. - 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 ```
returnsem 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 emwebhook_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 queissued_definitive_cardé escrita apenas emadmin_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.mde emRULES.md(última antes desta spec: R-006). Se a spec20260904175409for 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::FutureApprovedeixou de enviarmembership_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 doMembershipMailerremovido. 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 revertdo commit3356f03).Membership::FutureApprovevolta a enviarmembership_renewal_approved_emailpara 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:
- os documentos da filiação vigente são aprovados quando ela ainda é a única —
is_renewal?éfalse, o membro recebeuser_profile_documentation_approved_email; - o webhook cria a filiação seguinte com a flag ainda
false— o membro receberequest_documents_for_membership_email; - a carteira é emitida, a flag vira
true, oFutureApproveauto-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_emailpassou 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.
metadatado mailer — continua gravandometadata["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.erbdomembership_mailerestão zerados, não só este — é o padrão atual do repo, e não uma pendência criada por esta spec.