Execução do reparcelamento no provedor de pagamento

TLDR: Debits::Repayment troca a dívida por outra. Cria o parcelamento novo no Asaas e o débito novo aqui, apaga as cobranças do antigo e fecha o débito antigo como replaced. A negociação grava em provider_status a etapa que está sendo tentada antes de tentá-la, então uma queda deixa para trás o estado que diz onde retomar.

Contexto

O acordo aprovado (Negotiation) só vira cobrança quando executado no provedor. A execução tem duas chamadas externas e escritas locais entre elas, e dois pontos perigosos: o parcelamento pode nascer no Asaas e o processo morrer antes de registrarmos nada aqui; ou as cobranças antigas podem ser apagadas antes de o débito novo existir localmente.

Guardar esse progresso em memória não resolve, porque morre junto com o processo. Por isso ele vive numa coluna, negotiations.provider_status.

Os dois eixos

status e provider_status são independentes.

Coluna O que descreve Valores
status Ciclo de negócio do acordo simulated, approved, rejected, cancelled, expired
provider_status Execução no provedor pending, creating_repayment, deleting_old_charges, done

O status fica parado em approved durante toda a execução, o que permite ao retry passar pela mesma guarda sem caso especial.

Máquina de estados

mermaid stateDiagram-v2 direction LR [*] --> pending pending --> creating_repayment: start_repayment_creation! creating_repayment --> deleting_old_charges: repayment_created! deleting_old_charges --> done: old_charges_deleted! done --> [*]

O trabalho acontece dentro do estado, nunca na seta: primeiro grava, depois age.

Estado O que já é verdade O que acontece nele
pending Nada foi tocado —
creating_repayment O parcelamento pode existir no Asaas e ser desconhecido aqui Consulta pela referência, cria se não existir, lê as cobranças e cria o débito novo com suas parcelas e produtos
deleting_old_charges O débito novo existe dos dois lados Apaga as cobranças do parcelamento antigo e fecha o débito antigo
done Execução concluída —

A regra de ordem

O registro local nunca pode ficar atrás do provedor. Por isso o débito novo é criado ainda em creating_repayment, antes de qualquer apagamento. A ordem inversa abriria uma janela em que o Asaas tem um parcelamento vivo que o charges desconhece: dinheiro poderia entrar sem ter onde ser atribuído, e o débito antigo continuaria parecendo vivo aqui com parcelas já mortas lá.

A janela que sobra, entre criar o novo e fechar o antigo, é benigna: os dois débitos existem, as cobranças antigas ainda estão ativas e o cliente sempre tem como pagar alguma coisa.

Pelo mesmo motivo, cria-se no provedor antes de apagar: se a criação falhar, o cliente continua com a cobrança anterior ativa.

A troca

Ao final, a dívida mudou de lugar:

  Débito antigo Débito novo
status replaced awaiting_negotiation_payment
datas closed_at = agora opened_at = agora
payment_type inalterado repayment_first
parcelas intactas, como histórico criadas das cobranças do Asaas
produtos ficam cópia, com o progresso como está
provider_checkout_url inalterado link da 1ª cobrança, para o atendente enviar

Cliente, atendente, organization_slug, provider_customer_id e payment_provider são copiados. Cada parcela do débito novo guarda o próprio payment_link.

Contratos e negativações ficam no débito antigo.

Retomada

Uma falha não gera transição. A negociação permanece onde estava.

provider_status ao entrar O que a execução faz
pending Fluxo inteiro
creating_repayment Consulta pela referência antes de criar; pula a parte local se generated_debit_id já existir
deleting_old_charges Só apaga as cobranças antigas e fecha o débito antigo
done Failure(:already_provisioned)

A idempotência no provedor vem da referência nectar_negotiation_<id da negociação>, gravada em paymentExternalReference. O prefixo existe para nunca colidir com as referências do checkout-api, que divide a mesma conta do Asaas. Do lado local, o marcador é o generated_debit_id.

Recusas

Falha Quando
:invalid_negotiation Não aprovada, não é reparcelamento, ou 1ª parcela já vencida
:invalid_debit Débito fora de OPEN_STATUSES, ou sem os ids do provedor
:require_contract Débito já é repayment_first: o 2º reparcelamento exige confissão de dívida assinada, outro fluxo
:repayment_limit_reached Débito já é repayment_second: teto da RN-REPARC-1, o caminho é quitação
:payment_provider_account_not_found Sem PaymentProviderAccount para o organization_slug
:already_provisioned provider_status já em done
:asaas_error Falha no provedor; devolve provider_status e a mensagem

O modelo

Debit has_one :negotiation. A negociação aponta para o débito de origem (debit) e para o gerado (generated_debit), e é por ela que se navega entre os dois. Um débito é substituído no máximo uma vez, o que elimina a ambiguidade sobre qual parcelamento cancelar: é sempre o do próprio débito.

Onde cada coisa mora

  • As transições são métodos do Negotiation (start_repayment_creation!, repayment_created!, old_charges_deleted!) e do Debit (replace!). O model define como o próprio estado muda.
  • Debits::Repayment decide quando.
  • Asaas é tradutor: recebe o token pronto e não consulta model nenhum.

Ver backend_layers.md.

Limite conhecido

A partir de deleting_old_charges, a execução confia no que está gravado e não reconsulta o Asaas. Se alguém apagar o parcelamento novo direto no painel, o retry segue adiante e o cliente fica sem cobrança, em silêncio. A reação certa é o webhook PAYMENT_DELETED desfazer o estado. Pendência registrada em checkout_ignores_negotiation_payments.md.

Depois disto (outro PR)

1ª parcela paga: o débito novo vira negotiated. Vencida sem pagamento: vira pending e volta para a fila, e a negociação vira expired. As parcelas restantes de um acordo não pago seguem cobrando até um acordo novo substituí-las.

Ver também