Fix: renovação de filiação ignora vigência pendente de pagamento
TLDR:
WebhookMembershipService#set_valid_since_and_untilpassa a considerar qualquer filiação com vigência em andamento (valid_until >= hoje), independente do status de pagamento — excetorefunded— ao calcular a data de início de uma nova filiação. Hoje o filtro exigepayment_status: paid, então uma filiaçãopending/overdueainda vigente é tratada como se não existisse, e a nova filiação nasce com vigência a partir de hoje em vez de encadear depois do término da vigência atual.
Contexto
Toda vez que o webhook do gateway de pagamento envia status: "paid" para uma nova compra, WebhookMembershipService#initialize_new_membership cria uma nova Membership e chama set_valid_since_and_until (app/services/webhook_membership_service.rb:62-72) para definir valid_since/valid_until:
```ruby def set_valid_since_and_until last_paid_membership = @user.memberships.paid.valid_gte_today.order_by_newest_valid_until.try(:first)
if last_paid_membership.present? @membership.valid_since = last_paid_membership.valid_until @membership.valid_until = last_paid_membership.valid_until + 1.year else @membership.valid_since = Date.current.beginning_of_day @membership.valid_until = Date.current.end_of_month + 1.year end end ```
A query busca, entre as filiações do usuário, a mais recente que seja paid e ainda vigente (valid_until >= hoje, scope valid_gte_today). Se encontrar, a nova filiação encadeia a partir do valid_until dela. Se não encontrar, assume que não há nada em andamento e usa Date.current como ponto de partida.
Bug relatado: o checkout normal bloqueia compra para quem tem pendência financeira, mas durante campanhas essa restrição é flexibilizada para permitir compra com pendência no IBFT — configuração que não diferencia tipo de produto, então terapeutas com filiação em atraso conseguiram também comprar a renovação da filiação nessa janela. Essa lógica de checkout/campanha é externa a este repositório (confirmado: não há checkout, campanha ou bloqueio de pendência em app/ ou lib/ deste projeto — este serviço só reage ao status que o gateway envia).
Quando isso acontece, o usuário já tem uma filiação com vigência em andamento (ex.: ago/2025 a ago/2026) mas payment_status: pending ou overdue. Como o filtro .paid exige status pago, essa filiação vigente é ignorada pela busca, last_paid_membership vem nil, e a nova filiação é criada a partir de hoje — sobrepondo períodos em vez de continuar depois do término da vigência anterior. Foram identificados 2 casos até agora, corrigidos manualmente via console.
Histórico relacionado: R-002 documenta um fix anterior nesse mesmo método — removeu .admin_approved do encadeamento pelo motivo análogo (filiação paga mas ainda não aprovada pelo admin era ignorada). O padrão adotado ali foi afinar o filtro da query sem criar scope novo; esta correção segue o mesmo padrão.
Objetivos
- Ao criar uma nova filiação, se o usuário já tem qualquer filiação com vigência em andamento (
valid_until >= hoje), a nova filiação deve começar novalid_untildessa filiação — independente dopayment_statusserpaid,pendingouoverdue. - Preservar o comportamento atual para os demais casos: filiação
refunded(estornada) não conta como vigência em andamento; filiação vencida (valid_untilno passado) não conta, mesmo sepending/overdue; ausência de qualquer filiação anterior continua caindo noelse(Date.current).
Fora de escopo
- Não altera a lógica de checkout/campanha externa que permitiu a compra com pendência (fora deste repositório) — fora do escopo deste fix.
- Não cria scope novo em
Membership— troca inline na query, mesmo padrão do fix documentado em R-002. - Não corrige retroativamente as duas filiações já identificadas com vigência errada (já corrigidas manualmente via console) nem adiciona uma rotina de correção em massa — não há evidência de mais casos além dos 2 relatados.
- Não altera
admin_approvednem qualquer outra condição já corrigida em R-002.
Mudanças
app/services/webhook_membership_service.rb
Linha 63, dentro de set_valid_since_and_until:
diff
- last_paid_membership = @user.memberships.paid.valid_gte_today.order_by_newest_valid_until.try(:first)
+ last_paid_membership = @user.memberships.where.not(payment_status: :refunded).valid_gte_today.order_by_newest_valid_until.try(:first)
Mantém valid_gte_today e order_by_newest_valid_until como estão. payment_status é uma coluna string simples (db/schema.rb:228), sem enum Rails, com 4 valores em uso: pending, paid, overdue, refunded.
test/services/webhook_membership_service_test.rb
- Reescrever o teste
"sets valid_since from today when existing membership is not paid"(linhas 386-424), que hoje afirma como esperado o comportamento incorreto (filiaçãopendingvigente sendo ignorada). Passa a afirmar que a nova filiação encadeia a partir dovalid_untilda filiaçãopending. - Adicionar teste equivalente para
payment_status: overduevigente → encadeia normalmente. - Adicionar teste para
payment_status: refundedvigente (mesmo comvalid_untilno futuro) → continua caindo noelse,valid_sincea partir deDate.current. - Manter o teste existente de filiação
paidvigente (linhas 344-384) sem alteração de expectativa — guarda de não-regressão.
Como verificar
bash
make test test=test/services/webhook_membership_service_test.rb
make container.server.lint
Manual (console, somente leitura):
ruby
user = User.find_by(...) # usuário com filiação pending/overdue vigente
user.memberships.where.not(payment_status: :refunded).valid_gte_today.order_by_newest_valid_until.first
# deve retornar a filiação pendente vigente, não nil
Documentação
- R-002 — Renovação encadeia a partir de qualquer vigência em andamento não estornada — atualizada para refletir o estado atual da query (
.where.not(payment_status: :refunded).valid_gte_today) e indexada noRULES.md. - Aprendizado: renovação ignorava filiação com pagamento pendente — sobre o padrão recorrente: filtrar por
payment_status: paid(ouadmin_approved) ao buscar “a filiação vigente anterior” tende a excluir estados intermediários legítimos que ainda representam uma vigência real em andamento.