Fix: direcionamento de aprovação e ordenação da fila de emissão
TLDR:
User#membership_pending_approvalpassa a escolher, de forma determinística, a filiação paga semadmin_approved_atque vence mais cedo (voltando ao fallback atual só quando nenhuma estiver pendente); a fila de emissão (Membership.with_profile_approved_card_processing) passa a priorizar filiações vencendo no mês corrente e deixa de remover da fila filiações vencidas presas emprocessing.
Contexto
Existe uma regra de negócio para a fila de emissão de carteirinhas com dois critérios: filiações vencendo no mês corrente vão para o início da fila, independente da data de aprovação; as demais seguem a ordem de aprovação. Essa regra nunca chegou a ser implementada em Membership.with_profile_approved_card_processing (app/models/membership.rb:95) — a fila hoje ordena só por user_profiles.admin_approved_at ASC NULLS LAST (data do perfil, não da filiação) e usa valid_gte_today, que remove da fila qualquer filiação vencida, mesmo presa em processing sem nunca ter sido emitida.
Esse segundo ponto (valid_gte_today) se combina com um problema já parcialmente corrigido (f4bd837, #223): User#membership_pending_approval (app/models/user.rb:106) resolve qual filiação recebe a próxima aprovação de documentação, com fallback de current_membership para next_membership quando a vigente já foi aprovada antes. Essa implementação cobre corretamente os casos comuns, mas current_membership (app/models/user.rb:85) monta a busca sem ORDER BY:
ruby
def current_membership(date = Date.current)
@current_membership = memberships.paid.where("?::date between valid_since and valid_until", date).first
@current_membership || memberships.paid.order(created_at: :desc).first
end
Quando a filiação A vence no mesmo dia em que a filiação B (renovação) começa — o encadeamento padrão de renovação (valid_since de B = valid_until de A) —, a condição BETWEEN bate nas duas ao mesmo tempo nesse dia, e .first sem ordenação não garante qual das duas volta. Se a aprovação cair em B por acidente nesse dia, A (que estava vencendo naquele mês e ainda sem aprovação) nunca entra em processing — e mesmo que entrasse, seria removida da fila ao vencer, por causa do valid_gte_today.
Cenário A (documentação aprovada ainda dentro da vigência de A, A sem aprovação): a aprovação deve ir para A; A entra na fila priorizada pelo vencimento do mês.
Cenário B (A já aprovada e com carteira emitida): a aprovação deve ir para B; B entra na fila normal, ordenada por data de aprovação.
Objetivos
User#membership_pending_approvalescolhe, de forma determinística, a filiação paga (não arquivada) semadmin_approved_atque vence mais cedo — sem depender da ambiguidade decurrent_membershipno dia de virada de vigência.- Quando não existir nenhuma filiação paga sem
admin_approved_at, o comportamento atual é preservado: reaprova a filiação vigente por data (fallback existente viacurrent_membership, usado hoje no fluxo de reenvio de documentos sobre a mesma filiação). Membership.with_profile_approved_card_processingprioriza filiações que vencem no mês corrente (ordenadas porvalid_untildentro desse grupo) e, fora desse grupo, ordena pela data de aprovação da própria filiação — não mais a do perfil do usuário.- Filiação vencida presa em
processingpermanece na fila até ser emitida, em vez de desaparecer silenciosamente.
Fora de escopo
- Mudanças em
User#current_membership/User#next_membership— continuam com o significado atual (“vigente/próxima por data”), usados por outros fluxos (acesso Apolo, encadeamento de renovação no webhook). - Mudanças em
Membership::FutureApprove— já compara corretamentenext_membershipcom a filiação recebida. - Considerar
card_statusno critério de “já concluiu o fluxo” emmembership_pending_approval— o critério continua sendo sóadmin_approved_atpresente/ausente, como hoje. - A ambiguidade de desempate remanescente no fallback
current_membership(usado só quando nenhuma filiação está comadmin_approved_atem branco — ex.: reenvio de documento em que a única filiação, ou todas, já foram aprovadas antes) não é resolvida aqui: nesse caso as filiações envolvidas já concluíram o fluxo, então qual delas é reaprovada tem baixo impacto prático. Membership.with_profile_approved_not_issued(app/models/membership.rb:94) — não é usada em nenhum lugar do código hoje; não será alterada.
Mudanças
app/models/user.rb
Substitui o corpo de membership_pending_approval por uma busca direta e determinística, sem depender de current_membership para encontrar a filiação pendente — current_membership continua sendo chamado só como fallback final, exatamente como hoje:
ruby
def membership_pending_approval(date = Date.current)
memberships.paid.unarchived.where(admin_approved_at: nil)
.where("valid_until >= ?", date)
.order(:valid_until, :id).first || current_membership(date)
end
order(:valid_until, :id)elimina a ambiguidade do dia de virada: a filiação que está vencendo sempre temvalid_untilmenor que a próxima (que vence um ano depois), então a ordenação nunca empata de verdade nesse caso;:idcomo segundo critério só cobre um empate de datas fora do padrão normal de encadeamento.unarchivedevita que uma filiação arquivada apareça como candidata a receber aprovação.- Sem candidata pendente (todas já aprovadas), cai no fallback atual
current_membership(date)— preserva o reenvio de documentos sobre a mesma filiação (test/services/admin_user_profiles_service_test.rb:229, “re-approves the only membership when it is already approved”).
admin_approve_user_profile (app/services/admin_user_profiles_service.rb:18) não muda — já usa membership_pending_approval.
app/models/membership.rb
ruby
scope :with_profile_approved_card_processing, -> {
paid.card_in_processing.with_profile_approved
.reorder(Arel.sql("(date_trunc('month', memberships.valid_until) = date_trunc('month', current_date)) DESC, memberships.valid_until ASC, memberships.admin_approved_at ASC NULLS LAST"))
}
- Remove
valid_gte_today: filiação vencida presa emprocessingcontinua na fila até ser emitida. - Troca a ordenação de
user_profiles.admin_approved_at(do perfil, único por usuário, zerado a cada recusa de documento) paramemberships.admin_approved_at(da própria filiação em processamento). Arel.sql(...)é necessário porque a expressão não é uma referência simples de coluna (temdate_trunc) — o Rails recusa passar isso como string crua sem o wrapper. Fora isso, segue o padrão já usado nos outros scopes do arquivo (string única, sem heredoc).- Efeito colateral esperado ao subir: filiações antigas represadas em
processing(aprovadas há tempos, nunca emitidas, já vencidas) voltam a aparecer de uma vez na aba “Em Emissão” do admin — é o comportamento correto, mas vale avisar quem for revisar a fila depois do deploy.
Como verificar
bash
make test test=test/models/user_test.rb
make test test=test/services/admin_user_profiles_service_test.rb
make test test=test/models/membership_test.rb
make container.server.lint
Casos novos a cobrir:
| Teste | Cenário | Esperado |
|---|---|---|
User#membership_pending_approval |
A vence hoje (sem aprovação) e B começa hoje (B.valid_since == A.valid_until), nenhuma aprovada |
Retorna A, de forma determinística (repetir o teste não pode ser flaky) |
User#membership_pending_approval |
Todos os testes já existentes em test/models/user_test.rb:763-883 |
Continuam passando sem alteração (a nova implementação foi verificada contra cada um manualmente) |
Membership.with_profile_approved_card_processing |
Filiação X vence no mês corrente, filiação Y vence em outro mês mas foi aprovada antes | X vem antes de Y na fila |
Membership.with_profile_approved_card_processing |
Duas filiações vencendo no mês corrente, com valid_until diferentes |
Ordenadas por valid_until crescente entre si |
Membership.with_profile_approved_card_processing |
Filiação aprovada, em processing, com valid_until no passado |
Continua aparecendo na fila (antes era removida por valid_gte_today) |
Cenário manual (console):
ruby
user = User.find_by(email: "...")
user.memberships.order(:valid_since).map { |m| [m.valid_since, m.valid_until, m.admin_approved_at, m.card_status] }
user.membership_pending_approval
Documentação
- Criar
.project/docs/rules/membership/documentation_approval_targets_pending_membership.md(R-007): regra de qual filiação recebe a próxima aprovação de documentação. - Criar
.project/docs/rules/membership/emission_queue_prioritizes_current_month_due_date.md(R-008): regra dos dois critérios de ordenação da fila de emissão.