Importar só pagamentos com parcela em atraso

TLDR: o SQL de leitura do checkout passa a exigir, além do payments.status = 'overdue', ao menos uma parcela overdue que não foi deletada no gateway. Pagamento e parcela deletados no gateway são sempre desconsiderados, e o gateway_deleted é lido com COALESCE nos dois.

Continuação de Importação dos pagamentos atrasados do checkout como débitos, que só filtrava o status do pagamento.

Contexto

Hoje o import lê os pagamentos com payments.status = 'overdue', criados desde 01/01/2026 e com gateway_deleted falso ou nulo. As parcelas só são lidas depois, e a parcela deletada só é descartada na hora de copiá-las para o débito. Nada garante que o pagamento importado tem uma parcela atrasada e ativa. Um pagamento marcado como overdue cuja parcela atrasada foi deletada no gateway, ou que não tem parcela atrasada, viraria um débito sem nada a cobrar.

Objetivos

  • Importar só o pagamento que tem ao menos 1 parcela overdue e ativa.
  • Desconsiderar pagamento e parcela deletados no gateway, tanto para escolher o pagamento quanto para copiar as parcelas.
  • Ler gateway_deleted com COALESCE(..., false) = false, no pagamento e na parcela.
  • Manter a leitura paginada por keyset, numa consulta por página.

Fora de escopo

  • Remover ou alterar débitos já importados que não atendem à nova regra: o reimport só pula.
  • Mudar quais parcelas o débito recebe: continuam todas as ativas (paid, pending e overdue).
  • Mudar o corte de data (created_at >= since, 01/01/2026 00:00 em Brasília).
  • Índices no banco do checkout: o nectar só lê, e o import roda sob demanda.

Regras

Fonte: pagamentos do checkout

Entra o pagamento que atende a todos estes critérios:

  • payments.status = 'overdue'
  • payments.created_at >= since.in_time_zone.beginning_of_day (default 2026-01-01)
  • COALESCE(payments.gateway_deleted, false) = false
  • existe ao menos uma parcela do pagamento com installments.status = 'overdue' e COALESCE(installments.gateway_deleted, false) = false

Uma parcela deletada no gateway não conta para o EXISTS: um pagamento cuja única parcela atrasada foi deletada fica de fora.

SQL

Primeira página, com batch_size 500:

sql SELECT payments.*, organizations.id AS organization_id, organizations.slug AS organization_slug FROM payments INNER JOIN checkouts ON checkouts.id = payments.checkout_id INNER JOIN organizations ON organizations.id = checkouts.organization_id WHERE payments.status = 'overdue' AND payments.created_at >= '2026-01-01 03:00:00' AND COALESCE(payments.gateway_deleted, false) = false AND EXISTS (SELECT 1 FROM installments WHERE installments.payment_id = payments.id AND installments.status = 'overdue' AND COALESCE(installments.gateway_deleted, false) = false) ORDER BY payments.id ASC LIMIT 500

Da segunda página em diante o find_in_batches acrescenta AND payments.id > <último id da página anterior> antes do ORDER BY. A paginação, o join com organizations e o corte de fuso não mudam em relação à spec original.

Parcela ativa em um só lugar

Checkout::Installment.active passa a ser COALESCE(installments.gateway_deleted, false) = false, e ganha o scope overdue (installments.status = 'overdue'). O EXISTS do SQL e a leitura das parcelas de cada página usam esse mesmo scope, para “parcela ativa” nunca divergir entre as duas.

Mudanças

  • app/models/checkout/installment.rb: active com COALESCE, e o scope overdue.
  • app/models/checkout/payment.rb: o scope overdue_since passa a usar o COALESCE em payments.gateway_deleted e a exigir o EXISTS sobre Checkout::Installment.active.overdue correlacionado por payment_id. O payments.status = 'overdue' continua.
  • test/fixtures/checkout/installments.yml: parcelas para os pagamentos overdue que hoje não têm parcela atrasada (overdue_settlement, overdue_renewal, overdue_rafael, overdue_boundary_after, overdue_no_email, overdue_orphan_customer, overdue_no_document, overdue_no_reference, overdue_unsupported_provider), para os testes do import continuarem cobrindo cada motivo de pulo.
  • test/fixtures/checkout/payments.yml: casos novos (ver abaixo).
  • test/models/checkout/payment_test.rb: testes do scope.
  • test/use_cases/debits/import_overdue_from_checkout_test.rb: ajusta as contagens que dependem dos fixtures novos.

Casos novos de fixture:

Pagamento Situação Entra?
overdue_only_paid_installments status = overdue, parcelas só paid e pending não
overdue_deleted_installment_only status = overdue, única parcela overdue com gateway_deleted = true não
overdue_null_gateway_deleted status = overdue, gateway_deleted nulo, parcela overdue ativa sim
pending_with_overdue_installment status = pending, com parcela overdue ativa não (status)

Como verificar

  1. bin/rails t em modules/backend passa.
  2. Checkout::Payment.overdue_since(Date.new(2026, 1, 1)).to_sql gera o SQL da seção SQL, e o EXPLAIN não faz varredura sequencial de installments por pagamento (usa index_installments_on_payment_id).
  3. Em development, com CHECKOUT_DATABASE_URL no banco do checkout, comparar a contagem antes e depois: Checkout::Payment.overdue_since(since).count cai apenas pelos pagamentos overdue sem parcela atrasada ativa, e a diferença bate com a consulta direta no checkout.
  4. Rodar o job e conferir que nenhum débito novo tem parcela overdue ausente: Debit.where(provider_payment_id: ...) sem installments.status = 'overdue' dá zero para os importados depois desta mudança.

Documentação

  • .project/docs/rules/collections/checkout_overdue_import.md: nova redação da R-009.1 (parcela overdue ativa, COALESCE no gateway_deleted) e updated com a data do dia.
  • .project/docs/specs/20260928171636_import_overdue_checkout_payments.md: em Fonte, apontar para esta spec.
  • .project/docs/README.md: registrar esta spec e o futuro plano.