Negociação no mockup — calculadora IBFT dentro do drawer — Plano de implementação

TLDR: extrair o motor da calculadora para um módulo JS testado, inliná-lo no bundle do mockup, e reconstruir o Negociacao Drawer em duas colunas pré-preenchido pelo débito, com gerar/aprovar em dois estágios.

Spec: .project/docs/specs/20260930014047_negotiation_calculator_mockup.md Branch: feat/negotiation-calculator-mockup

Arquitetura: o motor de cálculo vira um módulo ES puro em tasks/mockup/negotiation_engine.js, sem DOM e sem estado global — é a fonte única e a única parte testável automaticamente. A ferramenta tasks/mockup/bundle.py desempacota o charges_mockup.html, inlina o motor no template.html e reempacota. As mudanças de layout do drawer são HTML dentro do bundle, com verificação manual no navegador conforme a lista da spec.

Stack: Python 3 (stdlib: json, base64, gzip) para o bundle; JavaScript ES2022 e o test runner nativo do Node 22 (node --test), sem dependência nova.

Restrições globais

  • Nenhuma dependência nova: node --test é nativo do Node 22 e a ferramenta de bundle usa só stdlib.
  • O motor não acessa DOM nem data do sistema: hoje e ref sempre entram como parâmetro, senão o teste é não-determinístico.
  • Valores monetários truncam com floor2, nunca arredondam — é o comportamento do Asaas.
  • Nenhum arquivo de modules/backend ou modules/frontend é tocado.
  • Negativadas não existem em lugar nenhum do motor (D8 da spec).

Mapa de arquivos

Arquivo Responsabilidade
tasks/mockup/bundle.py Desempacotar e reempacotar o charges_mockup.html, inlinando o motor
tasks/mockup/negotiation_engine.js Motor: aritmética, agregados, quitModel, gerarTexto, pré-preenchimento, validações
tasks/mockup/negotiation_engine.test.js Testes do motor
charges_mockup.html → template.html Estado, fluxo de negociação, botão do modal de detalhes, motor inlinado
charges_mockup.html → Negociacao Drawer.dc.html Layout em duas colunas e todas as seções

Task 1: Ferramenta de bundle

Files: - Create: tasks/mockup/bundle.py - Test: tasks/mockup/bundle_roundtrip.sh

Interfaces: - Produces: python3 tasks/mockup/bundle.py unpack <html> <dir> e python3 tasks/mockup/bundle.py pack <dir> <html>

  • [ ] Step 1: Teste que falha — bundle_roundtrip.sh desempacota, reempacota, desempacota de novo e exige que as duas árvores sejam idênticas (diff -r).
  • [ ] Step 2: Rodar — sh tasks/mockup/bundle_roundtrip.sh → FAIL (bundle.py não existe).
  • [ ] Step 3: Implementar — linha 375 é o manifest, 379 os ext_resources, 387 o template JSON. Assets compressed passam por gzip (com mtime=0 para determinismo); o nome do arquivo sai do ext_resources quando existe, senão é o uuid. No pack, se negotiation_engine.js existir em tasks/mockup/, seu conteúdo é inlinado no template.html entre os marcadores /* ENGINE:START */ e /* ENGINE:END */.
  • [ ] Step 4: Rodar → PASS.
  • [ ] Step 5: Commit — chore: ferramenta de bundle do mockup

Task 2: Motor — aritmética base

Files: - Create: tasks/mockup/negotiation_engine.js - Test: tasks/mockup/negotiation_engine.test.js

Interfaces: - Produces: floor2, parseV, jurosRate, diasAtraso, addDays, addMonths, jurosParcelaBruto, descJurosParcela, valorAtualizado, e as constantes JUROS_MES = 0.02, MULTA = 0.02, PISO = 500

Casos de teste, todos derivados das fórmulas da calculadora real:

Caso Entrada Esperado
floor2 trunca 2.3976 2.39
parseV pt-BR '1.206,28' 1206.28
jurosRate 184 dias 0.12266…
diasAtraso venc 2026-03-10, ref 2026-09-10 184
diasAtraso futuro venc 2026-12-01, ref 2026-09-10 0
addMonths clamp 2026-03-31 +1 / +2 / +3 2026-04-30 / 2026-05-31 / 2026-06-30
juros bruto 206.28, 184 dias 25.30
multa 206.28 4.12
valorAtualizado 206.28, 184 dias, sem desconto 235.70
valorAtualizado idem, desconto de juros 50% 223.05
  • [ ] Step 1: Escrever os testes acima (todos RED).
  • [ ] Step 2: node --test tasks/mockup/negotiation_engine.test.js → FAIL (Cannot find module).
  • [ ] Step 3: Implementar as funções, portadas 1:1 da calculadora. jurosRate(d) = (JUROS_MES/30)*d. valorAtualizado(p, ctx) = orig + floor2(orig*MULTA) + (jurosBruto − descJuros).
  • [ ] Step 4: → PASS.
  • [ ] Step 5: Commit — feat: aritmetica base do motor de negociacao

Task 3: Motor — agregados e vincSplit

Interfaces: - Consumes: Task 2 - Produces: pAtrasoTable, crossedVinc, pAtraso, aVencer, vincSplit, atrasoOriginal, atrasoMultaV, atrasoJurosV, descJurosTotal, atrasoComEnc, totalAtraso, totalVinc, totalGeral

Caso Entrada Esperado
soma do original 3 parcelas de 206.28 618.84
multa por parcela idem 12.36 (3 × 4.12, não floor2(618.84×0.02))
vincSplit ref 2026-09-10, 1ª a vencer 2026-07-15, qtd 3 2 vencidas (07-15, 08-15), 1 a vencer (09-15)
pAtraso inclui as cruzadas idem, com 1 em atraso na tabela 3 em atraso
a vencer no total atraso 618.84 + a vencer 412.56 total inclui as duas

A multa por parcela é o caso que separa o motor correto do mockup atual — 3 × floor2(206.28×0.02) = 12.36, enquanto floor2(618.84×0.02) = 12.37.

  • [ ] Step 1–5: mesmo ciclo. Commit — feat: agregados de parcelas do motor de negociacao

Task 4: Motor — quitModel e piso

Interfaces: - Consumes: Tasks 2–3 - Produces: quitModel(input) → { A, V, jurosRem, multaRem, vincDesc, dv, dg, geralDesc, subtotal, total, bruto, descTotal, abaixoDoPiso }

Caso Entrada Esperado
isenção de juros 3 × 206.28 em atraso, retJuros jurosRem = juros cheio; total sem ele
isenção de multa (só total) retMulta em quitação parcial multaRem = 0
clamp das a vencer descVincPerc = 25 aplica 10
clamp do geral descGeralPerc = 150 aplica 100
piso total resultante < 500 abaixoDoPiso = true
piso, limite total resultante = 500 abaixoDoPiso = false
sem negativada qualquer entrada nenhum campo neg* no retorno

abaixoDoPiso é o D9 da spec: o piso passa a valer sobre o total da quitação e rejeita a simulação, não arredonda. Quem consome decide como mostrar; o motor só sinaliza.

  • [ ] Step 1–5: commit — feat: motor de quitacao com piso de 500

Task 5: Motor — gerarTexto do reparcelamento

Interfaces: - Consumes: Tasks 2–4 - Produces: gerarTexto(input) → string

Caso Esperado no texto
sem desconto, 2 grupos itemiza atraso e a vencer; 📌 O *total* fica em
sem desconto, 1 grupo não itemiza; sufixo (N parcelas em atraso)
com desconto de juros 💵 *Valor atual da dívida:*, ✅ *Descontos aplicados:*, 💚 *Você economiza*
parcelado 💡 *Importante:* dividir em mais vezes *não gera juros*
1º reparcelamento CTA termina com *boleto* da primeira parcela
2º reparcelamento CTA cita *formulário rápido* e contrato de confissão de dívida
acesso 12m vigente expira no dia
acesso 12m expirado expirou no dia
extensão > 0, não expirado estendendo seu acesso até
vitalício Seu acesso ao material desse produto é *vitalício*
restrições bloco Enquanto houver um reparcelamento ativo: presente
  • [ ] Step 1–5: commit — feat: texto da proposta de reparcelamento

Task 6: Motor — gerarTexto da quitação

Caso Esperado no texto
quitação total sem benefício abertura neutra, sem “condição especial”
quitação total com desconto 📌 *Valor final para quitação:* e 💚 *Você economiza*
quitação parcial título Quitação parcial; 🔎 Nosso acordo é *exclusivamente financeiro*. sem bloco de acesso
CTA termina com Posso já gerar o seu *PIX*
sem bloco de restrições Enquanto houver um reparcelamento ativo ausente
fraseProdutos com 2 referente aos seus cursos de *A* e *B*
fraseProdutos com 3 *A*, *B* e *C*
  • [ ] Step 1–5: commit — feat: texto da proposta de quitacao

Task 7: Motor — pré-preenchimento pelo débito

Interfaces: - Produces: buildFromDebit(debit) → { atrasos, aVencer, produtos, acesso, expiracao }

Caso Entrada Esperado
separa por status installments overdue e upcoming duas listas
ignora pagas uma paid fora das duas
produtos 2 product_debits nomes na ordem recebida
acesso vitalício lifetime: true 'vital'
acesso 12m lifetime: false, expires_on '12m' com a data
livro lifetime: false, sem expires_on 'livro'
divergência linha editada divergente: true; contador { debito: 3, proposta: 2 }
  • [ ] Step 1–5: commit — feat: pre-preenchimento da negociacao pelo debito

Task 8: Motor — validações de R-001

Interfaces: - Produces: validar(input) → { bloqueios: [], alertas: [], reclassificar: 'agendamento' | null }

Regra Caso Esperado
RN-REPARC-1 payment_type = repayment_second bloqueio: sem novo reparcelamento
RN-REPARC-1 repayment_first livre
RN-REPARC-2 13 parcelas bloqueio
RN-REPARC-3 1ª vence em 8 dias reclassificar = 'agendamento'
RN-REPARC-3 1ª vence em 7 dias null
extensão > 90 dias alerta
RN-QUIT-4 total < 500 bloqueio
  • [ ] Step 1–5: commit — feat: validacoes das regras de negociacao

Task 9: Inlinar o motor e reescrever o estado no template.html

Files: - Modify: template.html (dentro do bundle)

Substituir o bloco simAtrasoCalc / simValorAtrasoSum / simJurosSum / simMultaSum / simTotal por chamadas ao motor inlinado. Novo estado negPgIdx guardando o débito em negociação. Fluxo de dois estágios: gerar proposta leva o débito a negociacao sem executar; aprovar executa; recusar devolve a pendente; cancelar e expirar idem. Rascunho não altera o status. A proposta guarda a composição inteira (D12), não só o texto.

Verificação: manual, itens 5, 6, 7, 12 e 13 da spec.

  • [ ] Commit — feat: motor de negociacao no mockup

Task 10: Botão do modal de detalhes por status

Files: - Modify: template.html

pendente e expirado → “Negociar”. negociacao → “Continuar negociação”, reabrindo em modo edição. pago e cancelado → sem botão.

Verificação: item 1 da spec.

  • [ ] Commit — feat: botao de negociacao por status da cobranca

Task 11: Drawer — card “Dados do débito”

Files: - Modify: Negociacao Drawer.dc.html

Card sem número no topo da coluna esquerda: cliente, CPF, produtos separados por vírgula, e as parcelas em atraso somente leitura. Contador de divergência.

Verificação: itens 2 e 9 da spec.

  • [ ] Commit — feat: card de dados do debito na negociacao

Task 12: Drawer — tabelas de atraso e a vencer

Files: - Modify: Negociacao Drawer.dc.html

Remover os três campos geradores. Tabela de atraso editável por linha, com marca de divergência e checkbox de seleção. Parcelas a vencer viram tabela com os mesmos controles. Remover coluna de negativada, “marcar todas” e o botão associado.

Verificação: itens 2, 5, 8 e 9 da spec.

  • [ ] Commit — feat: tabelas de parcelas da negociacao

Task 13: Drawer — descontos e acesso

Files: - Modify: Negociacao Drawer.dc.html

Descontos da quitação: isenção de juros, isenção de multa, a vencer (≤10%), geral. Sem controles de negativada. Acesso: “Expiração do acesso” pré-preenchida, mais “Extensão (dias)” e o alerta de > 90.

Verificação: itens 6 e 7 da spec.

  • [ ] Commit — feat: descontos e acesso na negociacao

Task 14: Drawer — duas colunas, resumo e proposta

Files: - Modify: Negociacao Drawer.dc.html

Layout em duas colunas: formulário à esquerda; resumo itemizado, grade 2x–12x e proposta fixos à direita. Preview com *negrito* renderizado, edição manual com aviso e botão de regerar. Rodapé com rascunho, gerar, aprovar (com confirmação), recusar e cancelar. Somente leitura quando aprovada.

Verificação: itens 6, 10, 11 e 12 da spec.

  • [ ] Commit — feat: layout em duas colunas da negociacao

Task 15: Verificação ponta a ponta

Percorrer os 13 itens de “Como verificar” da spec no navegador e registrar o resultado. O item 3 (juros pró-rata de ~12% numa parcela com seis meses de atraso) é o que prova que o motor foi portado, e o item 10 (texto igual ao da calculadora real para a mesma entrada) é o que prova que o gerador de texto está fiel.

  • [ ] Commit — chore: verificacao da negociacao no mockup

Cobertura da spec

Requisito Task
D5 paridade 1:1 do motor 2, 3, 4
D9 piso sobre o total 4
D8 sem negativadas 2–4 (ausência testada)
D4/D17 texto e edição manual 5, 6, 14
D6/D7/D10 pré-preenchimento 7, 12, 13
D11 divergência 7, 11, 12
D14 a vencer como tabela 3, 12
D15 quitação parcial 4, 12
D16 validações de R-001 8
D18/D20/D21 fluxo de estados 9, 10, 14
D19 rascunho não altera status 9
D12 composição guardada 9
D13 duas colunas 14
Verificação da spec 15