Execução do reparcelamento no provedor de pagamento
TLDR:
Debits::Repaymenttroca 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 comoreplaced. A negociação grava emprovider_statusa 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 doDebit(replace!). O model define como o próprio estado muda. Debits::Repaymentdecide 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.