Fila de emissão prioriza filiações vencendo no mês corrente
TLDR: O scope
Membership.with_profile_approved_card_processing(aba “Em Emissão” do admin) passa a colocar no topo da fila as filiações que vencem dentro do mês corrente; as demais continuam ordenadas por data de aprovação, como hoje.
Contexto
A fila de emissão de carteiras é a aba “Em Emissão” do admin (https://admin.citrg.com/admin?scope=em_emissao), alimentada pelo scope Membership.with_profile_approved_card_processing (app/models/membership.rb:95):
ruby
scope :with_profile_approved_card_processing, -> {
paid.card_in_processing.with_profile_approved.valid_gte_today
.reorder("user_profiles.admin_approved_at ASC NULLS LAST")
}
Existe uma regra de negócio para essa fila que nunca foi implementada: quem vence no mês corrente vai para o topo, independente da data de aprovação. A regra existe porque há casos em que a pessoa só consegue ter a documentação aprovada no mês em que a filiação está vencendo. Como o scope filtra por valid_gte_today, na virada do mês a filiação vencida sai automaticamente da lista — então é preciso destacá-la enquanto ainda está no período de vigência. Se a aprovação acontece no último dia do mês, aquela carteira ainda deve ser emitida; colocar esses casos no topo ajuda o emissor a identificá-los e concluí-los antes da virada.
Hoje a fila ordena só por user_profiles.admin_approved_at ASC, então uma filiação aprovada ontem e vencendo em três dias fica no fim da lista, atrás de dezenas de aprovações antigas com vencimento distante.
Só liberar a ordenação por clique nas colunas do ActiveAdmin não resolve: a ordenação por header é de coluna única e substitui o reorder inteiro (perdendo o desempate por data de aprovação), ordenar por valid_until ASC transforma a lista inteira numa fila por vencimento em vez de um bloco prioritário no topo, e o critério passa a depender do emissor lembrar de clicar a cada acesso. Prioridade de emissão é regra de negócio e deve viver no scope.
Uma versão anterior desta regra foi escrita em 20260903172011_fix_approval_target_and_emission_queue_order.md, junto com outras duas mudanças no mesmo scope (remover valid_gte_today e trocar o desempate para memberships.admin_approved_at). Aquela spec nunca chegou a ser implementada na parte de fila — o PR que saiu dela (f4bd837, #223) mexeu só em User#membership_pending_approval e foi revertido (1eec152, #225). Esta spec substitui a parte de ordenação da fila daquela spec, reduzida ao mínimo: só a priorização.
Objetivos
Membership.with_profile_approved_card_processingcoloca no topo da fila as filiações cujovalid_untilcai dentro do mês corrente.- Dentro e fora do grupo prioritário, o desempate continua sendo
user_profiles.admin_approved_at ASC NULLS LAST, exatamente como hoje. - O limite do mês é calculado no fuso da aplicação (
Brasilia), não no fuso do banco. - A regra fica documentada em
.project/docs/rules/membership/.
Fora de escopo
- Coluna ou badge “vence este mês” no index do admin. Decisão explícita do usuário: manter a mudança mínima, só a ordenação. O emissor vê a lista já na ordem certa; nada muda visualmente.
- Liberar ordenação por clique nos headers no scope
em_emissao. Oset_explicit_order(app/admin/memberships.rb:11) continua zerandoparams[:order]nesse scope, como hoje. Membership.with_profile_approved_not_issued(app/models/membership.rb:94), usado pelo scope “Pagas e não emitidas”: tem o mesmoreorder, mas fica fora desta mudança por decisão do usuário — só “Em Emissão” recebe a prioridade agora.- Remover
valid_gte_todaydo scope. Filiação vencida presa emprocessingcontinua saindo da fila na virada do mês, como hoje. A priorização reduz a janela de erro, não a elimina; tratar o represamento é outra discussão (tolerância de N dias após o vencimento, ou alerta). - Trocar o desempate de
user_profiles.admin_approved_atparamemberships.admin_approved_at. A spec anterior argumentava que a data do perfil é a errada (é única por usuário e zera a cada recusa de documento). Pode estar certo, mas é mudança de comportamento em cima de dados existentes e não foi pedida aqui — fica para uma spec própria.
Mudanças
app/models/membership.rb
ruby
scope :with_profile_approved_card_processing, -> {
paid.card_in_processing.with_profile_approved.valid_gte_today
.reorder(Arel.sql(Membership.sanitize_sql_array([
"CASE WHEN memberships.valid_until <= ? THEN 0 ELSE 1 END ASC, " \
"user_profiles.admin_approved_at ASC NULLS LAST",
Date.current.end_of_month
])))
}
Pontos de implementação:
- O
CASEvira a chave primária de ordenação e o critério atual (user_profiles.admin_approved_at ASC NULLS LAST) vira o desempate. O grupo0(vence neste mês) sobe inteiro para o topo; dentro dele, e dentro do grupo1, a ordem por data de aprovação é a mesma de hoje. - Não precisa de limite inferior no
CASE—valid_gte_todayjá removeu tudo que venceu antes de hoje, entãovalid_until <= fim do mêsé exatamente “vence dentro do mês corrente”. Date.current.end_of_monthcalculado em Ruby, nãocurrent_dateno Postgres. A aplicação roda emBrasilia(config/application.rb:30) e o banco em UTC: no último dia do mês, das 21h em diante,current_dateno Postgres já é o dia 1º do mês seguinte, e o grupo prioritário passaria a apontar para o mês errado justamente nas horas em que a regra mais importa.- Lambda, não constante: a expressão é montada a cada chamada do scope, então a virada do mês esvazia o grupo prioritário sozinha, sem restart nem cache.
Arel.sql(...)é necessário porque a expressão não é referência simples de coluna;sanitize_sql_arraymonta a string com o bind já interpolado e escapado. Não há entrada de usuário aqui, mas evita literal de data solto no SQL.
Nenhuma mudança em app/admin/memberships.rb.
Como verificar
bash
docker compose run --rm -e DATABASE_HOST=database -e RAILS_ENV=test runner bin/rails test test/models/membership_test.rb
make container.server.lint
make test test=<path>não roda nada neste repo (casa com uma regra vazia e sai 0). O alvo real émake run.test path=<path>, que depende deDATABASE_HOST=databaseno.env.
Casos novos a cobrir em test/models/membership_test.rb (mesmo padrão dos testes de scope já existentes em test/models/membership_test.rb:340, Membership.create! direto, Triple-A):
| Cenário | Esperado |
|---|---|
| X vence no mês corrente e foi aprovada hoje; Y vence daqui a 6 meses e foi aprovada há 3 meses | X vem antes de Y |
| Duas filiações vencendo no mês corrente, aprovadas em datas diferentes | Entre si, ordenadas por admin_approved_at crescente |
| Duas filiações vencendo em meses futuros, aprovadas em datas diferentes | Entre si, ordenadas por admin_approved_at crescente (comportamento atual preservado) |
| Filiação vencendo no último dia do mês corrente, aprovada hoje | Está no grupo prioritário (limite superior inclusivo) |
| Filiação vencendo no dia 1º do mês seguinte | Não está no grupo prioritário |
O caso do dia 1º do mês seguinte precisa de travel_to para uma data de referência fixa (ex.: Time.zone.local(2026, 9, 20)), senão o teste fica dependente do dia em que roda.
Cenário manual (console):
ruby
Membership.with_profile_approved_card_processing
.limit(15)
.map { |m| [m.register_number_pad, m.valid_until, m.user.user_profile.admin_approved_at] }
As primeiras linhas devem ser todas com valid_until dentro do mês corrente.
Documentação
- Criar
.project/docs/rules/membership/emission_queue_prioritizes_current_month.md(R-010): quem vence no mês corrente vai para o topo da fila de emissão; as demais seguem a ordem de aprovação. Registrar o porquê (a filiação sai da fila na virada do mês porvalid_gte_today, então precisa ser emitida enquanto ainda vigente). - Registrar R-010 em
.project/docs/RULES.mde no índice.project/docs/README.md.