Aprovação com múltiplas filiações pendentes

TLDR: quando um terapeuta tem mais de uma filiação pendente de aprovação, o sistema deve aprovar manualmente a filiação vigente (A) e auto-aprovar automaticamente as filiações futuras (B, C…), além de marcar issued_definitive_card = true no momento da aprovação dos documentos.

Status: proposed Created: 2026-08-11 Owner: @Ruan1800


Contexto

O fluxo de aprovação de documentos (AdminUserProfilesService#admin_approve_user_profile) chama user.active_memberships.first para selecionar qual filiação aprovar. O scope active ordena por created_at DESC e o método encadeia .order(created_at: :asc) — mas em Rails, .order acrescenta ao invés de substituir, então a ordenação efetiva é DESC. O .first retorna a filiação mais recente (B), não a vigente (A).

Caso concreto: - Filiação A — criada 03/02/2026, vigência 2026–2027 - Filiação B — criada 03/07/2026, vigência 2027–2028 - Em 22/07/2026, admin aprova os documentos → sistema aprova B, A fica sem aprovação

Adicionalmente, issued_definitive_card = true hoje só é setado quando a carteira física entra em remessa (admin_issue_card). O requisito é que ele seja setado no momento em que o atendente aprova os documentos e o status vai para :processing.

Relacionado a R-004 — que documenta o fluxo de auto-aprovação via webhook para quem já tem carteira definitiva.

Objetivos

  • Sempre aprovar a filiação cuja vigência cobre a data de hoje (valid_since <= hoje <= valid_until), excluindo reembolsos
  • Após aprovar a filiação vigente (A), auto-aprovar automaticamente todas as filiações futuras pendentes (B, C…) ancoradas em valid_since >= A.valid_until
  • Manter o envio de dois e-mails: aprovação manual para A e membership_renewal_approved_email para cada futura
  • Setar issued_definitive_card = true no momento em que admin_approve_membership é chamado (card_status → :processing)
  • Garantir atomicidade: aprovação de A e auto-aprovação de futuras dentro de uma única transação

Não-goals

  • Não altera o fluxo de aprovação via webhook (WebhookMembershipService) — intocado
  • Não cria use case, camada nova ou arquivo novo — mudanças cirúrgicas nos arquivos existentes
  • Não remove issued_definitive_card = true de admin_issue_card — permanece lá também (idempotente)
  • Não seta documentation_status = :ok em auto_approve! — segue o padrão do webhook, que não seta

Mudanças

app/models/membership.rb

Adiciona método de estado auto_approve!, seguindo o padrão de suspend!/unsuspend!:

ruby def auto_approve!(approver) self.card_status = :auto_issued self.admin_approved_at = Time.current self.admin_approved_by = approver self.card_issued_at = Time.current self.card_issued_by = approver save! end

app/services/admin_memberships_service.rb

Em admin_approve_membership, adiciona 1 linha após setar card_status = :processing:

ruby resource.user.update!(issued_definitive_card: true)

app/services/admin_user_profiles_service.rb

Refatora apenas admin_approve_user_profile:

Seleção da filiação vigente — substitui active_memberships.try(:first) por current_membership, já existente em User (user.rb:76), que encontra a filiação cuja vigência cobre hoje (between valid_since and valid_until):

ruby @membership = @profile_user.current_membership

Transação + auto-aprovação da próxima — envolve aprovação de A e auto-aprovação da futura em ActiveRecord::Base.transaction. E-mails ficam fora da transação.

Reutiliza User#next_membership (user.rb:83), já existente, que encontra a filiação que começa após o valid_until da atual. Guard contra o fallback || current do método:

```ruby ActiveRecord::Base.transaction do # aprova UserProfile e membership vigente (A) future_membership = @profile_user.next_membership if future_membership && future_membership != @membership && future_membership.admin_approved_at.nil? && future_membership.not_issued? future_membership.auto_approve!(automatic_approver) end end

e-mails fora da transação

fire_approved_user_profile_email if future_membership && future_membership != @membership MembershipMailer .with(user_id: @profile_user.id, membership_id: future_membership.id) .membership_renewal_approved_email .deliver_later end ```

Adiciona automatic_approver como método privado — mesma lógica já existente no webhook, sem tocar nele:

```ruby private

def automatic_approver User.find_or_create_by!(email: “automatico@citrg.com.br”) do |u| u.name = “Automático” u.admin = true pass = SecureRandom.hex u.password = u.password_confirmation = pass end end ```

Como verificar

bash make test test=test/services/admin_user_profiles_service_test.rb make test test=test/models/membership_test.rb make container.server.lint

Cenário manual (console):

```ruby # usuário com A (vigente) e B (futura), ambas sem aprovação user = User.find_by(email: “…”) user.memberships.order(:valid_since) # => [A (not_issued, admin_approved_at: nil), B (not_issued, admin_approved_at: nil)]

simula aprovação pelo admin

service = AdminUserProfilesService.new service.admin_approve_user_profile(user.user_profile, User.admins.first)

user.memberships.reload.order(:valid_since).map { |m| [m.valid_since, m.card_status, m.admin_approved_at] } # => A: [:processing, ], B: [:auto_issued, ] user.reload.issued_definitive_card # => true ```

Guards da query de futuras — casos a verificar:

Cenário Esperado
Terapeuta com só A (sem futuras) Loop vazio, sem e-mail extra, sem efeito colateral
A + B pendentes A aprovada manualmente, B auto-aprovada, 2 e-mails
A + B + C pendentes A manual, B e C auto-aprovadas em ordem, 3 e-mails
B já aprovada manualmente antes admin_approved_at não nil → excluída da query
B reembolsada payment_status: refunded → excluída da query
B com carteira em processamento not_issued falso → excluída da query

Documentação