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 Drawerdocharges_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 Drawercom 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-001eR-002aparecem 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 clampnegClampede três ramos dogerarTexto. - Atualizar
RN-REPARC-4no repo real (ver Decisões abaixo). Fica registrado como pendência, não executado aqui. - Ligar parcela a produto no schema.
installmentsnão tem vínculo comproduct_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))
addMonthscom 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 porvaiEstudarequitSemBeneficio;fraseProdutos()derivada deproduct_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 porlifetimeeexpires_on. Mapeamento de acesso:lifetime: true→ vitalício;lifetime: falsecomexpires_on→ 12 meses;lifetime: falsesemexpires_on→ livro. Os dois primeiros saem direto do schema; o terceiro é suposição —product_debitsnão tem nenhum campo que represente “material físico sem acesso”, e a ausência deexpires_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
pendenteeexpirado. Débito emnegociacaomostra “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 parapendente. 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-2já coberta pela grade 2x–12x;RN-REPARC-3reclassifica em agendamento quando a 1ª parcela vence em mais de 7 dias;RN-REPARC-1inferida deDebit.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:
- Botão por status — na tela Cobrança, abrir Detalhes numa linha
pendente: o botão “Negociar” aparece. Empagoecancelado: não aparece. Emexpirado: aparece. Emnegociacao: aparece “Ver proposta” no lugar. - 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.
- 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.
- Multa e truncamento — conferir que a multa é 2% por parcela e que os centavos são
truncados, não arredondados (
floor2). - A vencer no total — aumentar as parcelas a vencer e conferir que o total muda. Hoje não muda.
- 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.
- 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.
- Quitação parcial — desmarcar parcelas e conferir que o total e o texto refletem só as selecionadas.
- Divergência — alterar o valor de uma linha e conferir a marca na linha e o contador no card do débito.
- 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.
- 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.
- Dois estágios — gerar a proposta e conferir que o débito foi para
negociacaosem 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. - Desfechos — recusar uma negociação e conferir que o débito volta para
pendentee aceita nova proposta.
Documentação
.project/docs/README.md— incluir esta spec no índice despecs/.- 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çãodiscount_only_on_settlementemmodules/backend/app/models/negotiation.rbseguirá 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.