Negociação no mockup — calculadora IBFT dentro do drawer, pré-preenchida pelo débito

TLDR: portar o motor da calculadora IBFT — juros pró-rata por dia, quitação com descontos, e o gerador de texto da proposta — para dentro do Negociacao Drawer do charges_mockup.html, pré-preenchido pelos dados do débito. Negativadas ficam de fora. O drawer passa a abrir pela tela de Cobrança e ganha dois estágios: gerar proposta (não executa nada) e aprovar negociação (executa). Isto é desenho de mockup; o app real vira uma spec separada.

Contexto

A calculadora IBFT é a ferramenta que os atendentes usam hoje para montar propostas de negociação. Ela é inteiramente manual: o atendente digita quantas parcelas estão em atraso, o vencimento da mais antiga e o valor original, e a calculadora gera as linhas a partir disso.

O charges_mockup.html já tem um Negociacao Drawer que é uma cópia parcial e divergente dessa calculadora. As divergências não são cosméticas:

# Mockup hoje Calculadora
1 juros = orig × 0.02 fixo floor2(orig × (0.02/30) × diasAtraso), pró-rata por dia, sem teto
2 multa sobre a soma, sem truncamento 2% por parcela, com floor2
3 parcelas a vencer não entram no total entram
4 “Quitação” é só um label no select motor próprio (quitModel) com descontos e piso
5 aprovação = extensão > 90 dias + clamps, piso, coerência

A divergência nº 1 é a mais grave: uma parcela seis meses atrasada tem 12% de juros, não 2%.

O app real está ainda mais distante — modules/frontend/src/hooks/customers/negotiationForm.constants.ts usa STATIC_OVERDUE_INTEREST = 14.82 e STATIC_OVERDUE_FINE = 14.82, valores fixos em reais independentes do valor da parcela e do atraso, e grava as propostas em localStorage. Não existe controller nem rota de negotiations; o motor de negociação (USER-015) está documentado como não implementado.

O que já existe de real e não muda aqui: o model Negotiation (com MAX_INSTALLMENTS = 12, os enums de status e provider_status, e a validação discount_only_on_settlement) e o use case Debits::Repayment, que executa no Asaas um acordo já aprovado (reference/negotiation/repayment_execution.md).

A oportunidade que motiva esta spec: o débito já tem todos os dados que o atendente digita à mão. Debit has_many :installments (number, amount_cents, updated_amount_cents, interest_cents, due_on, status) e has_many :product_debits (product_name, lifetime, expires_on). Parcelas em atraso, parcelas a vencer, produtos e tipo de acesso saem todos do débito.

Objetivos

  • Portar o motor de cálculo da calculadora para o Negociacao Drawer com paridade 1:1, incluindo o gerador de texto da proposta.
  • Pré-preencher o drawer a partir do débito, eliminando os campos geradores.
  • Abrir o drawer pela tela de Cobrança, a partir de um débito.
  • Separar gerar proposta (registro, não executa) de aprovar negociação (executa o acordo).
  • Desenhar como as regras de R-001 e R-002 aparecem na interface.

Fora de escopo

  • O app real (modules/backend, modules/frontend). Vira uma spec separada depois que o desenho for validado no mockup.
  • Parcelas negativadas. Serão tratadas em outro ponto do sistema. Isso remove do motor: o checkbox por linha, a coluna thNegat, o “marcar todas como negativadas”, o desconto na dívida negativada (descNegPerc), a isenção de juros da negativada (isentarJurosNeg), o clamp negClamped e três ramos do gerarTexto.
  • Atualizar RN-REPARC-4 no repo real (ver Decisões abaixo). Fica registrado como pendência, não executado aqui.
  • Ligar parcela a produto no schema. installments não tem vínculo com product_debits; a limitação é aceita e documentada.
  • Criar task no Asana para esta mudança.

Decisões

Fechadas em entrevista; registradas porque várias contrariam o que existe hoje.

# Decisão
D1 Alvo é o mockup. O app real vira spec separada.
D2 Desconto de juros continua permitido no reparcelamento, contrariando RN-REPARC-4 e a validação discount_only_on_settlement. A calculadora está em uso e a distinção entre desconto de principal e desconto de juros é a prática real. RN-REPARC-4 precisa ser atualizada no repo real — pendência registrada, fora do escopo desta spec.
D3 Escopo cobre reparcelamento (1º e 2º) e quitação (total e parcial).
D4 O texto da proposta segue o modelo da calculadora real (gerarTexto), inteiro.
D5 Paridade 1:1 do motor. Consequência direta de D4: gerarTexto lê quitModel() campo a campo (bruto, descTotal, jurosRem, multaRem, vincDesc, dv, geralDesc, dg), então não existe o texto sem o motor.
D6 Os três campos geradores (quantidade / venc. da mais antiga / valor original) saem. O débito preenche a tabela; o atendente ajusta linha a linha.
D7 Produto e tipo de acesso vêm de product_debits.
D8 Negativadas fora de escopo.
D9 O piso de R$ 500 passa a valer sobre o total da quitação, rejeitando a simulação. Na calculadora ele só era aplicado ao desconto da negativada; sem esta decisão ele desapareceria junto com D8. Segue RN-QUIT-4, que é certainty: high e diz que o piso é absoluto.
D10 Campo de acesso vira “Expiração do acesso”, lido de product_debits.expires_on. A conta dataLib + 12 meses deixa de existir. Sobrescrevível pelo atendente.
D11 Divergência entre a cópia de trabalho e o débito é sinalizada: marca na linha alterada e contador no card do débito.
D12 A proposta guarda a composição inteira, não só o texto, para permitir reabertura e edição.
D13 Drawer alargado, duas colunas: formulário à esquerda, resumo e proposta fixos à direita.
D14 Parcelas a vencer viram tabela, uma linha por parcela, como as em atraso.
D15 Quitação parcial se resolve com checkbox por linha. Os “produtos extras” manuais da calculadora somem.
D16 As regras de R-001 viram validação na tela, com RN-REPARC-3 reclassificando em agendamento em vez de rejeitar.
D17 Edição manual do texto entra inteira: flag de “editado”, botão de regerar e preview do *negrito*.
D18 Gerar proposta não executa nada. Coloca o débito em negociacao e a proposta segue editável. Aprovar negociação é o único ponto que executa o reparcelamento.
D19 Rascunho não altera o status do débito; o botão existe.
D20 “Aprovar negociação” fica no rodapé do drawer, com confirmação. Depois de aprovada, o drawer reabre somente leitura.
D21 Entram também os desfechos recusada e cancelada, além de expirada, para o débito não ficar preso em negociacao.

Mudanças

O charges_mockup.html é um bundle empacotado (manifest base64+gzip + template JSON). Editar exige desempacotar, alterar os arquivos internos e reempacotar. O ciclo unpack → repack foi validado com round-trip byte a byte idêntico.

Arquivos internos do bundle:

Negociacao Drawer.dc.html

  • Reestruturar para duas colunas (D13): formulário à esquerda; resumo, grade de parcelamento e proposta fixos à direita.
  • Novo card “Dados do débito”, sem número, no topo da coluna esquerda: cliente, CPF, produtos separados por vírgula, e a lista de parcelas em atraso somente leitura. É a superfície de conferência. Contador de divergência (D11).
  • Seção “Parcelas em atraso”: remover os três campos geradores (D6). A tabela continua editável por linha — valor, vencimento, remover, adicionar parcela manual — e é a cópia de trabalho da proposta. Marca de divergência por linha (D11). Checkbox de seleção para quitação parcial (D15). Remover a coluna de negativada, o “marcar todas” e o botão associado (D8).
  • Seção “Parcelas a vencer”: virar tabela, uma linha por parcela (D14), com os mesmos controles e o checkbox de seleção.
  • Seção de descontos (quitação): isenção de juros, isenção de multa, desconto nas a vencer (≤10%) e desconto geral. Remover os controles de negativada (D8).
  • Seção “Acesso e extensão”: substituir “Data de liberação do acesso” por “Expiração do acesso”, pré-preenchida (D10). Manter “Extensão (dias)” e o alerta de > 90 dias.
  • Seção “Proposta gerada”: preview com *negrito* renderizado, edição manual com aviso e botão de regerar (D17).
  • Rodapé: “Salvar rascunho”, “Gerar proposta” e, quando já houver proposta gerada, “Aprovar negociação” (com confirmação), “Aluno recusou” e “Cancelar negociação” (D20, D21). Drawer somente leitura quando a negociação estiver aprovada.

template.html — motor de cálculo

Substituir o bloco simAtrasoCalc / simValorAtrasoSum / simJurosSum / simMultaSum / simTotal pelo motor da calculadora:

JUROS_MES = 0.02 · MULTA = 0.02 · PISO = 500 floor2(v) = Math.floor(v * 100) / 100 jurosRate(d) = (JUROS_MES / 30) * d diasAtraso(venc) = max(0, refCalc − venc) // refCalc = venc. da 1ª parcela do acordo jurosParcela(p) = floor2(orig * jurosRate(diasAtraso(p.venc))) valorAtualizado(p) = orig + floor2(orig * MULTA) + (jurosParcela(p) − descJurosParcela(p))

  • addMonths com clamp de último dia do mês (31/03 → 30/04, nunca dois vencimentos no mesmo mês).
  • quitModel() sem as partes de negativada (D8): isenção de juros e multa do atraso, desconto nas a vencer com clamp em 10%, desconto geral com clamp em 100%, e o piso de R$ 500 sobre o total (D9), rejeitando a simulação.
  • vincSplit() — parcelas a vencer que cruzam a data de pagamento entram como atraso.
  • gerarTexto() completo (D4, D17), incluindo: tom de abertura por vaiEstudar e quitSemBeneficio; fraseProdutos() derivada de product_debits; itemização só quando há mais de um grupo; lista de descontos linha a linha; economia em R$ e %; bloco de acesso variando entre parcial / 12m / vitalício / livro; texto de extensão diferente conforme o acesso já tenha expirado; bloco de restrições só no reparcelamento; CTA variando entre PIX (quitação), formulário + contrato (2º reparcelamento) e boleto (1º reparcelamento).

template.html — pré-preenchimento e fluxo

  • Pré-preencher a partir do débito (D6, D7): parcelas em atraso e a vencer com valor e vencimento próprios; produto por product_name; tipo de acesso por lifetime e expires_on. Mapeamento de acesso: lifetime: true → vitalício; lifetime: false com expires_on → 12 meses; lifetime: false sem expires_on → livro. Os dois primeiros saem direto do schema; o terceiro é suposição — product_debits não tem nenhum campo que represente “material físico sem acesso”, e a ausência de expires_on é a leitura mais próxima. A escolha importa porque muda três blocos do texto da proposta; confirmar antes de levar ao app real.
  • Novo estado guardando qual débito está em negociação, já que o modal de detalhes fecha ao abrir o drawer.
  • Botão “Negociar” no rodapé do modal de detalhes apenas para débitos pendente e expirado. Débito em negociacao mostra “Continuar negociação”, que reabre a proposta existente em modo edição (D18) — não é visualização. O rótulo diferente existe para avisar, antes do clique, que é continuação e não proposta nova: só existe uma negociação por débito.
  • Gerar proposta → débito para negociacao, nada executado, proposta editável (D18). Aprovar → executa. Recusar → volta para pendente. Cancelar e expirar idem (D21). Rascunho não altera o status (D19).
  • Guardar a composição inteira da proposta, não só o texto (D12).
  • Validações de R-001 (D16): RN-REPARC-2 já coberta pela grade 2x–12x; RN-REPARC-3 reclassifica em agendamento quando a 1ª parcela vence em mais de 7 dias; RN-REPARC-1 inferida de Debit.payment_type (repayment_first / repayment_second).

Catálogo de produtos

Os 13 produtos da calculadora não precisam ser portados: o produto vem de product_debits.product_name e o tipo de acesso de lifetime / expires_on (D7, D10).

Limitação aceita: installments não tem vínculo com produto no schema. Num débito com vários product_debits, a proposta cita todos os produtos do débito mesmo quando as parcelas selecionadas são de um só. Resolver isso exige mudança de schema no backend, fora do escopo.

Como verificar

Abrir o charges_mockup.html reempacotado no navegador e percorrer:

  1. Botão por status — na tela Cobrança, abrir Detalhes numa linha pendente: o botão “Negociar” aparece. Em pago e cancelado: não aparece. Em expirado: aparece. Em negociacao: aparece “Ver proposta” no lugar.
  2. Pré-preenchimento — clicar em “Negociar” e conferir que as tabelas de atraso e a vencer já vêm preenchidas com os valores e vencimentos do débito, que o produto está selecionado e que a expiração do acesso foi preenchida.
  3. Juros pró-rata — conferir que uma parcela com seis meses de atraso recebe ~12% de juros, e não 2%. É a divergência nº 1 do Contexto e o teste mais direto de que o motor foi portado.
  4. Multa e truncamento — conferir que a multa é 2% por parcela e que os centavos são truncados, não arredondados (floor2).
  5. A vencer no total — aumentar as parcelas a vencer e conferir que o total muda. Hoje não muda.
  6. Quitação — trocar o tipo para Quitação, aplicar isenção de juros e multa e 10% nas a vencer, e conferir o resumo itemizado e a economia.
  7. Piso de R$ 500 — aplicar desconto geral suficiente para o total cair abaixo de R$ 500 e conferir que a simulação é rejeitada, não arredondada.
  8. Quitação parcial — desmarcar parcelas e conferir que o total e o texto refletem só as selecionadas.
  9. Divergência — alterar o valor de uma linha e conferir a marca na linha e o contador no card do débito.
  10. Texto da proposta — conferir que o texto gerado bate com o da calculadora real para a mesma entrada, incluindo o bloco de acesso e o CTA correto por tipo de proposta.
  11. Edição manual — editar o texto à mão, conferir o aviso de “editado”, mexer num valor e conferir que a edição não foi sobrescrita, e que “regerar” volta ao automático.
  12. Dois estágios — gerar a proposta e conferir que o débito foi para negociacao sem que nada tenha sido executado; reabrir, alterar o parcelamento, e conferir que a alteração persiste. Depois aprovar e conferir que o drawer reabre somente leitura.
  13. Desfechos — recusar uma negociação e conferir que o débito volta para pendente e aceita nova proposta.

Documentação

  • .project/docs/README.md — incluir esta spec no índice de specs/.
  • Pendência registrada, não executada nesta spec: RN-REPARC-4 (rules/negotiation/installment_renegotiation.md) contradiz D2 e precisa ser atualizada quando a mudança chegar ao app real. A validação discount_only_on_settlement em modules/backend/app/models/negotiation.rb seguirá o mesmo destino. Alterar a regra agora, com a mudança existindo só no mockup, deixaria o doc descrevendo um comportamento que o código do app ainda rejeita.