- **Rateio de Órfãos Pré-005**: O script ix_orphan_payment_step7.cjs ignora txs 'Encargos de atraso (rateio retroativo)' para evitar re-rateio duplo e valida que o valor de encargos nunca exceda 99% do total da tx (Guarda anti-regressão Wade/Alexander).
Resources
33Install
npx skillscat add gilvans/fintechbankapp/docs Install via the SkillsCat registry.
SKILL: Regras de Negócio — Fatura e Pagamento do Cartão de Crédito
Versão compacta para agentes de IA. Consulte
docs/REGRAS-NEGOCIO-FATURA.mdpara a referência completa.
⚠️ Regra Nº 1 — Antes de TUDO
🔗 Consulte as seções 2.1 e 2.2 no documento completo para a regra detalhada.
NUNCA use computeInvoiceGross() como valor da fatura fechada!
❌ ERRADO: _closedInvoiceValorTotal = computeInvoiceGross(inv) // = R$ 4.284,94
✅ CORRETO: _closedInvoiceValorTotal = inv.valor_total // = R$ 3.870,86computeInvoiceGross = valor_total + multa + juros_mora + juros_rem + iof — inclui ENCARGOS CONGELADOS. Usar isso como "total da fatura" causa dupla cobrança.
1. Estrutura de Dados — Tabela invoices
🔗 Consulte a seção 7.1 no documento completo para a tabela completa com tipos e restrições.
| Coluna | Descrição |
|---|---|
valor_total |
PRINCIPAL — compras do ciclo (NUNCA inclui encargos) |
valor_pago |
Quanto já foi pago desta invoice |
data_pagamento |
Timestamp de quando foi totalmente quitada |
multa, juros_mora, juros_remuneratorios, iof |
Encargos CONGELADOS no momento do seed/fechamento |
saldo_anterior |
Herança de encargos de fatura anterior |
2. Duas Funções, Dois Propósitos
🔗 Consulte a seção 2.2 no documento completo para a tabela de usos completa com 11 contextos + diagrama.
| Função | Fórmula | Para que serve |
|---|---|---|
computeInvoiceGross(inv) |
valor_total + saldo_anterior + iof + multa + juros_mora + juros_rem |
Filtrar invoices com saldo > 0; Auditar overpayment |
computeInvoiceOwed(inv) |
Math.max(0, valor_total - valor_pago) |
Saldo devedor; Base de encargos; Target de quitação |
Regra de ouro:
| Contexto | Use | NUNCA use |
|---|---|---|
closedInvoice (saldo devedor) |
valor_total - valor_pago |
computeInvoiceGross |
_closedInvoiceValorTotal (badge PAGA) |
valor_total |
computeInvoiceGross |
| Base de cálculo de encargos | valor_total - valor_pago |
computeInvoiceGross |
Target de quitação (planDistribution) |
valor_total |
computeInvoiceGross |
Filtro .filter(i => ... > 0) |
computeInvoiceGross |
computeInvoiceOwed |
| Auditoria de overpayment | computeInvoiceGross |
computeInvoiceOwed |
3. Ciclo da Fatura
🔗 Consulte a seção 1.1 no documento completo para detalhes e estados da fatura.
| Evento | Dia | Descrição |
|---|---|---|
| Fechamento | 20 | Fatura atual → FECHADA. Novas compras → próximo ciclo |
| Vencimento | 10 | Data limite para pagamento sem encargos |
| Grace Period | +3 dias | Dias úteis após vencimento antes de bloquear cartão |
4. Tipos de Pagamento
🔗 Consulte a seção 3.1 no documento completo para o fluxo de pagamento completo + tratamento de erro.
| Tipo | Condição | INSERT description | Merchant no Frontend |
|---|---|---|---|
| Total | amount >= totalDue - 0.01 |
(via cardRepo.payDueInstallments) |
"Pagamento fatura (Total)" |
| Mínimo | amount >= 10% de totalDue |
'Pagamento minimo de fatura' |
"Pagamento fatura (Mínimo)" |
| Parcial | amount < 10% de totalDue |
'Pagamento parcial de fatura' |
"Pagamento fatura (Parcial)" |
| Antecipação | Parcelamento antecipado | 'Antecipacao de parcelas' |
"Antecipacao de parcelas" |
Código da diferenciação (index.cjs ~4645):
const payDescription = payAmount >= minPayment
? 'Pagamento minimo de fatura'
: 'Pagamento parcial de fatura';5. Mapeamento PAYMENT (BD → Frontend)
🔗 Consulte as seções 8 e 6.4 no documento completo para regras de display e consistência visual.
A transação INVOICE_PAYMENT passa por 5 estágios:
INSERT → SELECT → .map() → Filtro por janela → Renderização ReactRegras críticas do mapeamento:
- PAYMENT aparece na fatura ABERTA (
openTransactions), NUNCA na fechada currentInvoiceNÃO inclui PAYMENT —filter(tx => tx.type !== 'PAYMENT')- Amount no .map() é
Math.abs(amount)— sempre positivo - Ordem dos matches importa:
'parcial'é verificado ANTES de'minimo'
Código do mapeamento (index.cjs ~411):
const lowerDesc = (desc || '').toLowerCase();
if (lowerDesc.includes('parcial')) {
merchant = 'Pagamento fatura (Parcial)';
} else if (lowerDesc.includes('minimo') || lowerDesc.includes('mínimo')) {
merchant = 'Pagamento fatura (Mínimo)';
} else {
merchant = 'Pagamento fatura (Total)';
}6. Encargos — Regra de Herança
🔗 Consulte a seção 4 no documento completo para cenários de herança, regras de exibição e fluxo de pagamento em atraso.
Encargos da fatura fechada NUNCA aparecem no total da fechada. São HERDADOS pela fatura aberta.
| Fórmula | Cálculo |
|---|---|
| Fórmula | Cálculo |
| :-------- | :-------- |
| Multa (2%) | residual × 0,02 |
| Juros Mora (0,0333%/dia) | residual × 0,000333 × dias |
| Juros Rem (0,513%/dia) | residual × 0,00513 × dias |
| IOF (0,38% fixo + 0,0082%/dia) | residual × 0,0038 + residual × 0,000082 × dias |
Base de cálculo = SEMPRE o residual (valor_total - valor_pago). NUNCA o valor_total bruto.
Função consolidada: calcAllCharges(principal, days) → objeto com { multa, jurosMora, jurosRemuneratorios, iofAdicional, iofDiario, iof, total }
🔗 Consulte a Seção 17 no documento completo para as 7 funções com exemplos numéricos e tabela de valores de referência para 0 a 60 dias.
7. Regra do Amount no Frontend
🔗 Consulte a seção 3.3 no documento completo para exemplos de código corretos e errados.
O amount DEVE ser passado como 3º parâmetro em TODAS as chamadas:
// ✅ CORRETO:
const res = await payCreditCardInvoice(cpf, pin, amountSelecionado);
// ❌ ERRADO (bug já ocorrido):
// const res = await payCreditCardInvoice(cpf, pin);
// → backend assume totalDue, quebra pagamento parcial!8. Funções que Você PODE Chamar
🔗 Consulte as seções 11 e 12 no documento completo para código fonte completo de cada função.
| Função | Local | Retorna |
|---|---|---|
getClosedInvoiceDebt(cpf) |
index.cjs:4340 |
{ owed, invoices, invoice, oldest } |
settleClosedInvoices(cpf, nowIso, payAmount) |
index.cjs:4360 |
void (atualiza BD) |
computeInvoiceGross(inv) |
utils/invoiceMath.js:27 |
Number (principal + encargos) |
computeInvoiceOwed(inv) |
utils/invoiceMath.js:39 |
Number (só principal - pago) |
computeInvoicePaidInfo(inv) |
utils/invoiceMath.js:110 |
{ isPaid, paidAt } |
planDistribution(rows, amount) |
utils/invoiceMath.js:62 |
{ applied, remaining, allPaid, invoices } |
9. Bugs Já Corrigidos (NÃO REPETIR)
🔗 As correções estão documentadas nas seções 2.1, 3.3 e 6.4 do documento completo.
| Bug | O que aconteceu | Correção |
|---|---|---|
| Double-counting de encargos | _closedInvoiceValorTotal usava computeInvoiceGross → R$ 4.284,94 em vez de R$ 3.870,86 |
Trocar para parseFloat(inv.valor_total) |
| Amount não passado | InvoicesView.tsx chamava payCreditCardInvoice(cpf, pin) sem amount |
Adicionar pendingAmount como 3º parâmetro |
| Minimum = Partial | Ambos usavam 'Pagamento parcial de fatura' → mostravam (Parcial) |
Adicionar payAmount >= minPayment → 'Pagamento minimo de fatura' |
| currentInvoice inflado | PAYMENT entrava na soma de compras | Adicionar filter(tx => tx.type !== 'PAYMENT') |
| Encargos congelam após pagamento parcial (v2) | runBillingValidation usava calcAllCharges(residual, days) - existing → diff negativo → nada inserido |
Mudar para incremento DIÁRIO: multa/IOF adicional = one-time; juros/IOF diário = calc(residual, 1) sempre. NUNCA comparar total vs existing |
| Encargos perdidos no pagamento parcial (v1) | runBillingValidation deletava + reinseria encargos, perdendo acumulado |
Mudar para acumulação: consultar existente, inserir só delta positivo. NUNCA deletar |
10. Scripts de Auditoria
🔗 Consulte a seção 9.1 no documento completo para regras de consistência adicionais.
| Comando | Finalidade |
|---|---|
node scripts/audit_completo.js |
Double-counting + negative balance |
node scripts/audit_completo.js --fix --confirm |
Corrigir automaticamente |
npm run audit:all |
Auditoria completa + relatório HTML |
11. Documentação Completa
Consulte docs/REGRAS-NEGOCIO-FATURA.md para:
- Código completo de todas as funções
- Exemplos numéricos detalhados
- Fluxo da tela de pagamento PIX/Boleto
- Jobs agendados e cronograma
- Pipeline de transformação PAYMENT
12. Troubleshooting — Erros Comuns
🔗 Esta seção também está disponível na Seção 16 do documento completo, com explicações mais detalhadas.
12.1 EADDRINUSE: address already in use 0.0.0.0:3001
Causa: O servidor API anterior não foi encerrado corretamente.
Soluções:
# 1. Matar o processo na porta 3001 (Windows)
netstat -ano | findstr :3001
taskkill /PID <PID> /F
# 2. Ou usar PowerShell:
Get-Process -Id (Get-NetTCPConnection -LocalPort 3001).OwningProcess | Stop-Process
# 3. Ou no bash (Git Bash):
curl -X POST http://localhost:3001/api/admin/shutdown # se o servidor responderPrevenção: Sempre usar Ctrl+C no terminal do servidor antes de reiniciar, ou matar o processo explicitamente.
12.2 Acesso negado (403) em rotas admin
Causa: Token do admin expirado. O token JWT tem tempo de vida limitado.
Solução:
# Obter novo token de admin:
curl -s -X POST http://localhost:3001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"cpf":"99999999999","password":"admin999"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4Prevenção: A cada nova sessão, obter um token fresco. Se estiver usando uma variável $ADMIN_TOKEN, renová-la.
12.3 Adjacent JSX elements must be wrapped in an enclosing tag (Vite/Babel)
Causa: O bug clássico de template literal em JSX — uma crase + > sem } antes em uma className.
// ❌ ERRADO: backtick + > sem o } da expressão JSX
<div className={`text-${cor} ← FALTA o } antes de >
>`} ← Babel interpreta o > como JSX, não como parte da string
// ✅ CORRETO:
<div className={`text-${cor} ${ativo ? 'font-bold' : ''}`}>Solução: Rodar o lint customizado ou verificar visualmente todas as className com template literals.
node WEB/scripts/lint-no-missing-jsx-brace.cjsPrevenção: Manter className em uma única linha sempre que possível. Evitar quebras de linha dentro de crases de className.
12.4 Saldo insuficiente ao pagar fatura
Causa: O balance do usuário é menor que o valor solicitado para pagamento.
Verificação:
curl -s http://localhost:3001/api/users/me -H "Authorization: Bearer $TOKEN" |
python -c "import sys,json; u=json.load(sys.stdin)['user']; print(f'Balance: R\$ {u[\"balance\"]}'); print(f'ClosedInvoice: R\$ {u.get(\"creditCard\",{}).get(\"closedInvoice\")}')"Solução: Depositar saldo via admin:
curl -s -X POST http://localhost:3001/api/admin/deposit \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cpf":"12312312312","amount":5000}'12.5 valor_pago > valor_total (double-counting)
Causa: Pagamento foi registrado em TODAS as faturas (multi-invoice) em vez de distribuído proporcionalmente.
Verificação:
node API/scripts/audit_completo.jsSolução:
node API/scripts/audit_completo.js --fix --confirmPrevenção: A função settleClosedInvoices deve usar planDistribution para distribuir o valor entre invoices (da mais antiga para a mais recente).
12.6 Fatura fechada mostra valor maior que o pago
Causa: _closedInvoiceValorTotal está usando computeInvoiceGross(inv) em vez de inv.valor_total.
Verificação:
curl -s http://localhost:3001/api/users/me -H "Authorization: Bearer $TOKEN" |
python -c "import sys,json; u=json.load(sys.stdin)['user']['creditCard']; print(f'ValorTotal: R\$ {u.get(\"_closedInvoiceValorTotal\")}'); print(f'ValorPago: R\$ {u.get(\"_closedInvoiceValorPago\")}'); print(f'Gross seria: R\$ {u.get(\"closedInvoice\",0) + u.get(\"daysOverdue\",0)*0.02*3870.86:.2f} (estimado)')"Correção: Em enrichUserCreditCardData, trocar computeInvoiceGross(inv) por parseFloat(inv.valor_total || 0).
12.7 Erro de Unicode no console Windows (charmap codec can't encode)
Causa: O terminal Windows (cmd.exe) usa codificação cp1252 que não suporta alguns caracteres Unicode como →, ✓, ⚠️.
Solução:
# Executar antes de rodar scripts:
chcp 65001 # Muda para UTF-8Prevenção: Usar caracteres ASCII puros em scripts (=> em vez de →, [OK] em vez de ✅) ou usar PowerShell em vez de cmd.exe.
12.8 Porta 3000 já em uso (frontend)
Causa: Outra instância do Vite está rodando (possível de sessão anterior).
Solução:
# Matar o processo do Node na porta 3000
netstat -ano | findstr :3000
taskkill /PID <PID> /F
# Ou iniciar em outra porta:
npm run dev -- --port 517312.9 Cannot find module '../repositories/dbAdapter'
Causa: O cronReconciliation.js depende de um módulo que não existe mais.
Solução (temporária): O servidor inicia mesmo com este erro — é apenas o cron de conciliação que falha. Para remover o erro, comentar a linha que importa cronReconciliation.js em index.cjs.
12.10 Testes falham com timeout (Vitest no Windows)
Causa: O Vitest pode exceder o timeout padrão no Windows devido a I/O mais lento.
Solução:
npm test -- --testTimeout=30000Prevenção: Aumentar o timeout no jest.config.js ou vitest.config.js:
module.exports = {
testTimeout: 30000, // 30 segundos
};12.11 Python não encontrado (python3 vs python)
Causa: No Windows, o comando é python, não python3.
Solução: Usar python em vez de python3 em todos os scripts e comandos.
Verificação:
python --version # Deve funcionar no Windows12.12 Ripgrep binary not found
Causa: O ripgrep (rg) não está instalado no sistema Windows ou o path não está configurado.
Solução:
# Instalar via npm:
npm install -g @microsoft/ripgrep
# Ou configurar variável de ambiente:
set CODEBUFF_RG_PATH=C:\caminho\para\rg.exeAlternativa: Usar grep do Git Bash ou findstr no PowerShell no lugar de comandos ripgrep.
13. Frontend — Payload e Exibição
🔗 Consulte as seções 5 e 6 no documento completo para exemplos de código e regras visuais detalhadas.
Payload de pagamento:
// ✅ CORRETO: amount como 3º parâmetro
const res = await payCreditCardInvoice(cpf, pin, amountSelecionado);
// ❌ ERRADO: sem amount → backend assume totalDue
// const res = await payCreditCardInvoice(cpf, pin);Regras de exibição da fatura fechada PAGA:
- Mostrar valor ORIGINAL com badge verde "✓ PAGA"
- Exibir
closedInvoicePaidAt(data do pagamento) - NÃO mostrar botões "Pagar", "PIX", "Boleto"
- Usar
_closedInvoiceValorTotal(valor_total), NUNCA computeInvoiceGross
Flags de display:
| Campo | Uso |
|---|---|
closedInvoiceIsPaid |
Badge verde PAGA |
_closedInvoiceValorTotal |
Valor original preservado |
_closedInvoiceValorPago |
Quanto foi pago |
closedInvoicePaidAt |
Data do pagamento |
closedInvoiceCharges |
Encargos de referência |
daysOverdue |
Dias em atraso |
14. Motor de Validação Diária — runBillingValidation
🔗 Consulte a seção 10 no documento completo para o fluxo completo com código, cron e rotas da API.
Função: async function runBillingValidation() em API/index.cjs:4048
O que faz:
- Carrega
billing_confige calcula ciclo atual - Busca TODOS os usuários com fatura fechada não paga
- Para cada um: calcula encargos sobre residual (
valor_total - valor_pago) - Atualiza
account_statusedays_overdueno banco - Sincroniza
dias_atrasonas invoices
Fórmulas de encargos (sobre o residual):
| Encargo | Fórmula |
|---|---|
| Multa (2%) | residual × 0,02 |
| Juros Mora (0,0333%/dia) | residual × 0,000333 × dias |
| Juros Rem (0,513%/dia) | residual × 0,00513 × dias |
| IOF (0,38% + 0,0082%/dia) | residual × (0,0038 + 0,000082 × dias) |
Rotas: POST /admin/billing/validate-all (admin) | POST /admin/billing/run-cycle
🚨 REGRA CRÍTICA — Incremento Diário sobre Residual (NUNCA Comparar Total)
🔗 Consulte a Seção 10 no documento completo para a referência completa do motor de validação.
O motor NUNCA deleta encargos acumulados. Cada execução ADICIONA o incremento de 1 dia sobre o residual ATUAL. Encargos de multa (2%) e IOF adicional (0,38%) são cobranças ÚNICAS. Juros de mora, juros remuneratórios e IOF diário são incrementos DIÁRIOS.
⛔ O Problema (target vs existing)
// ANTIGO (🐛) — depois de corrigir DELETE+INSERT, ainda usava:
const target = calcAllCharges(residual, daysOverdue); // total sobre residual
const diff = target - existing; // target MENOR que existing após pagamento parcial!
if (diff > 0.005) INSERT diff;
// ← diff = NEGATIVO → NUNCA insere nada → encargos CONGELAMProblema: calcAllCharges(1000, 21) = R$ 146,80, mas existente = R$ 519,81. diff = -R$ 372,01 → encargos CONGELAM mesmo com residual devedor. O cliente para de acumular juros e o banco para de cobrar. ❌
✅ A Solução — Incremento Diário
Cada tipo de encargo tem um comportamento específico:
| Tipo | Comportamento | Base de Cálculo | Exemplo (Dia 21, residual R$ 1.000) |
|---|---|---|---|
| Multa (2%) | 🔒 Única vez | calcMulta(valorTotal) original |
3870,86 × 0,02 = R$ 77,42 (insere 1×) |
| Juros Mora (0,0333%/dia) | 📈 Diário | calcJurosMora(residual, 1) |
1000 × 0,000333 × 1 = R$ 0,33 (sempre insere) |
| Juros Rem (0,513%/dia) | 📈 Diário | calcJurosRem(residual, 1) |
1000 × 0,00513 × 1 = R$ 5,13 (sempre insere) |
| IOF Adicional (0,38%) | 🔒 Única vez | calcIofAdicional(valorTotal) original |
3870,86 × 0,0038 = R$ 14,71 (insere 1×) |
| IOF Diário (0,0082%/dia) | 📈 Diário | calcIofDiario(residual, 1) |
1000 × 0,000082 × 1 = R$ 0,08 (sempre insere) |
Código atual (API/index.cjs:4399):
// ── MULTA: uma única vez sobre o valor_total ORIGINAL ──
if (getExisting('multa') < 0.005) {
const multa = calcMulta(originalValorTotal);
INSERT INTO billing_charges (amount = multa);
}
// ── IOF: primeira vez = adicional+diário; subsequente = só diário ──
if (getExisting('iof') < 0.005) {
const iof = calcIof(originalValorTotal, daysOverdue); // + diário acumulado
INSERT INTO billing_charges (amount = iof);
} else {
const dailyIof = calcIofDiario(residual, 1); // só 1 dia
INSERT INTO billing_charges (amount = dailyIof);
}
// ── JUROS MORA + REM: sempre incremento de 1 dia sobre residual ──
const dailyJurosMora = calcJurosMora(residual, 1);
const dailyJurosRem = calcJurosRemuneratorios(residual, 1);
INSERT INTO billing_charges (amount = dailyJurosMora);
INSERT INTO billing_charges (amount = dailyJurosRem);Regras
| # | Regra |
|---|---|
| 1 | NUNCA use DELETE em billing_charges com status pending |
| 2 | Multa e IOF adicional são ÚNICAS — verificar getExisting() antes de inserir |
| 3 | Juros mora, juros rem e IOF diário são INCREMENTOS DIÁRIOS — sempre inserir calc(residual, 1) |
| 4 | calcAllCharges(residual, daysOverdue) NÃO DEVE SER USADO para comparar vs existing — use o incremento de 1 dia |
| 5 | calcAllCharges(original, daysOverdue) é usado APENAS na PRIMEIRA inserção de IOF (que combina Adicional + Diário) |
| 6 | totalLineCharges é apenas para logging — o cálculo real usa SUM(amount) no banco |
| 7 | Se residual = 0, incrementos diários = 0, nada é inserido — sem custo |
Exemplo Numérico (Correto)
Dia 1-20: Motor acumulou R$ 519,81 em encargos (R$ 3.870,86 × 20 dias)
Dia 20: Cliente pagou R$ 2.870,86 → residual = R$ 1.000,00
Dia 21: Motor roda:
Multa: já existe (R$ 77,42) → SKIP ✅
Juros Mora: 1000 × 0,000333 × 1 = R$ 0,33 → INSERT ✅
Juros Rem: 1000 × 0,00513 × 1 = R$ 5,13 → INSERT ✅
IOF: já existe → calcIofDiario(1000, 1) = R$ 0,08 → INSERT ✅
Total hoje: R$ 5,54
→ R$ 519,81 + R$ 5,54 = R$ 525,35 acumulado ✅
Dia 22: Motor roda:
Juros Mora: + R$ 0,33
Juros Rem: + R$ 5,13
IOF Diário: + R$ 0,08
Total hoje: + R$ 5,54
→ R$ 530,89 acumulado ✅
Dia 30 (10 dias após pagamento):
Acumulado: R$ 519,81 + (10 × R$ 5,54) = R$ 575,21 ✅
Em vez de... congelado em R$ 519,81 (🐛 antigo)
Diferença para o banco: R$ 55,40 recuperados! 🎯Comportamento Antigo (Histórico)
1ª versão (🐛): DELETE + INSERT → perdia encargos acumulados a cada execução
2ª versão (🐛): target vs existing → diff negativo após pagamento parcial → congela
3ª versão (✅): Incremento diário → sempre adiciona 1 dia sobre residual atual15. Função enrichUserCreditCardData — Montagem do creditCard
🔗 Consulte a seção 13 no documento completo para o passo a passo completo e diagrama de fluxo.
Função: async function enrichUserCreditCardData(normalized, cpf) em API/index.cjs:155
🎯 FONTE ÚNICA de Encargos: billing_charges
A partir da correção v3, enrichUserCreditCardData LÊ os encargos acumulados da tabela billing_charges (INSERTED pelo runBillingValidation) em vez de recalcular calcAllCharges(residual, daysOverdue). Isso resolve o bug onde após pagamento parcial o residual menor fazia calcAllCharges retornar valores menores que o acumulado real.
Fluxo de leitura de encargos:
1. SELECT charge_type, SUM(amount) FROM billing_charges
WHERE cpf=X AND status='pending' GROUP BY charge_type
2. Se encontrou E !isPaid → usa valores REAIS:
multa = SUM WHERE charge_type='multa'
jurosMora = SUM WHERE charge_type='juros_mora'
jurosRem = SUM WHERE charge_type='juros_remuneratorios'
iof = SUM WHERE charge_type='iof'
totalEncargos = round2(multa + jurosMora + jurosRem + iof)
3. Fallback (fatura paga ou sem billing_charges):
buildClosedInvoiceSummary(...)Etapas de execução:
1. Carregar invoices do banco (invRows)
2. Montar fatura fechada:
├── closedInvoice = Σ valor_total (original, imutável)
├── closedInvoiceResidual = Σ (valor_total - valor_pago) ← saldo devedor
└── _closedInvoiceValorTotal / _closedInvoiceValorPago ← badge PAGA
3. Buscar transações (cardRows + pendingInstallments)
4. Mapear transações → formato frontend
5. Filtrar openTransactions (inclui PAYMENT)
6. Calcular currentInvoice (exclui PAYMENT)
7. ⭐ LER encargos do billing_charges (fonte única)
8. Calcular daysOverdue em tempo real
9. Montar currentInvoiceTotal = currentInvoice + residual + encargosRegras críticas:
currentInvoiceNUNCA é alterado — permanece = compras do cicloclosedInvoice= valor ORIGINAL (imutável), NUNCA gross- Encargos somam a
currentInvoiceTotal, NÃO acurrentInvoice - PAYMENT incluso em
openTransactions, NÃO emclosedTransactions - ⭐ Encargos NUNCA são recalculados — lidos do billing_charges ou de paidLateCharges
- ⭐ daysOverdue é sempre calculado EM TEMPO REAL (independente do residual)
16. Pipeline PAYMENT e Cronograma
🔗 Consulte as seções 14 e 15 no documento completo para o mapeamento completo INSERT → Frontend e lista de jobs.
Pipeline PAYMENT (5 estágios)
INSERT → SELECT → .map() → Filtro janela → Renderização| Estágio | Campo | Comportamento |
|---|---|---|
| INSERT | amount |
NEGATIVO (-3870.86) — crédito |
| INSERT | description |
'Pagamento minimo...' / 'Pagamento parcial...' |
| .map() | amount |
Math.abs() → "+R$ 3.870,86" |
| .map() | merchant |
parcial > minimo > Total |
| Render | badge | Verde (PAYMENT) |
Jobs Agendados
| Job | Frequência |
|---|---|
| Invoice Engine | Diário 00:00 |
| runBillingValidation | Diário 00:05 |
| Recurring Engine | Diário 00:10 |
| Sync Dias Atraso | Diário 00:15 |
| Fix Pagamentos Órfãos | Semanal dom 03:00 |
| Auditoria Completa | Semanal dom 02:00 |
17. Regra CREDIT_CARD vs ACCOUNT_DEBIT — Pagamento de Conta Recorrente
🔗 Detalhe completo: seção 20 do documento completo
Regra essencial
ACCOUNT_DEBIT |
CREDIT_CARD (default) |
|
|---|---|---|
| Consome | users.balance |
credit_card_available_limit |
| Transação | PAYMENT (Débito em Conta) |
PAYMENT (Faturado no Cartão) |
| Sem fundos | 400 SALDO_INSUFICIENTE |
400 LIMITE_INSUFICIENTE |
| Fatura aberta | Nunca aparece | Só linha informativa (informative: true) |
Regras críticas
- NUNCA polui a fatura como gasto — PAYMENT/SUBSCRIPTION ficam fora do
cardRows; o total da fatura não muda. CREDIT_CARDfaturado no cartão aparece na aberta como linha informativa com merchant real (ex.:Assinatura Netflix Mensal),informative: true, sem somar no total.ACCOUNT_DEBITvai só para o extrato da home — nunca na fatura (nem informativo).- Automático (
recurringEngine) gravaSUBSCRIPTION; manual (POST /recurring-bills/:cpf/:billId/pay) gravaPAYMENT— diferença intencional. - Upsert idempotente: pagar 2x a mesma conta atualiza a MESMA linha (billId do frontend), sem duplicar.
- Retentativa automática: falha →
past_due+1 dia; após 3 →suspended.
- Rateio de Órfãos Pré-005: O script ix_orphan_payment_step7.cjs ignora txs 'Encargos de atraso (rateio retroativo)' para evitar re-rateio duplo e valida que o valor de encargos nunca exceda 99% do total da tx (Guarda anti-regressão Wade/Alexander).