feat: implement robust valor_pago architecture and financial sync hardening for Portal and Manager
This commit is contained in:
parent
f6022fd0fc
commit
ed52d6a2fa
|
|
@ -387,6 +387,10 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
payments: sorted,
|
payments: sorted,
|
||||||
studentId: sorted[0].studentId,
|
studentId: sorted[0].studentId,
|
||||||
totalAmount: sorted.reduce((sum, p) => sum + Number(p.amount), 0),
|
totalAmount: sorted.reduce((sum, p) => sum + Number(p.amount), 0),
|
||||||
|
totalReceived: sorted.reduce((sum, p) => {
|
||||||
|
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase());
|
||||||
|
return sum + (isPaid ? (Number((p as any).valor_pago) || (Number(p.amount) - (Number(p.discount) || 0))) : 0);
|
||||||
|
}, 0),
|
||||||
totalInstallments: sorted[0].totalInstallments || sorted.length,
|
totalInstallments: sorted[0].totalInstallments || sorted.length,
|
||||||
description: sorted[0].description?.split(' (')[0] || 'Parcelamento',
|
description: sorted[0].description?.split(' (')[0] || 'Parcelamento',
|
||||||
dueDate: sorted[0].dueDate
|
dueDate: sorted[0].dueDate
|
||||||
|
|
@ -1055,6 +1059,9 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-6 py-5">
|
||||||
<div className="font-black text-slate-900">R$ {group.totalAmount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
<div className="font-black text-slate-900">R$ {group.totalAmount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
||||||
|
{group.totalReceived > 0 && (
|
||||||
|
<div className="text-[10px] text-blue-600 font-black">PAGO: R$ {group.totalReceived.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
||||||
|
)}
|
||||||
<div className="text-[10px] text-slate-500 font-medium">Total do Carnê</div>
|
<div className="text-[10px] text-slate-500 font-medium">Total do Carnê</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-5">
|
<td className="px-6 py-5">
|
||||||
|
|
@ -1402,7 +1409,22 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
{p.installmentNumber && <div className="text-[9px] text-slate-400">{p.installmentNumber}/{p.totalInstallments}</div>}
|
{p.installmentNumber && <div className="text-[9px] text-slate-400">{p.installmentNumber}/{p.totalInstallments}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">{new Date(p.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
<td className="px-4 py-3">{new Date(p.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
||||||
<td className="px-4 py-3">R$ {p.amount.toFixed(2)}</td>
|
<td className="px-4 py-3">
|
||||||
|
<div className="font-bold text-slate-700">
|
||||||
|
R$ {(() => {
|
||||||
|
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase());
|
||||||
|
const valorPago = (p as any).valor_pago ? Number((p as any).valor_pago) : 0;
|
||||||
|
const bruto = Math.max(Number(p.amount), Number((p as any).amount_original || 0));
|
||||||
|
|
||||||
|
if (isPaid && valorPago > 0) return valorPago.toFixed(2);
|
||||||
|
if (isPaid && p.discount > 0) return (bruto - p.discount).toFixed(2);
|
||||||
|
return bruto.toFixed(2);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase()) && (p as any).valor_pago > 0 && (
|
||||||
|
<div className="text-[9px] text-indigo-500 font-bold">Líquido Recebido</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3">{getStatusBadge(p)}</td>
|
<td className="px-4 py-3">{getStatusBadge(p)}</td>
|
||||||
<td className="px-4 py-3 text-right flex justify-end gap-2">
|
<td className="px-4 py-3 text-right flex justify-end gap-2">
|
||||||
{p.asaasPaymentId && (
|
{p.asaasPaymentId && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function massSync() {
|
||||||
|
console.log('--- Iniciando Sincronização em Massa (SQL -> JSON) ---');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { rows: dbPayments } = await pool.query('SELECT asaas_payment_id, valor_pago FROM alunos_cobrancas WHERE valor_pago > 0');
|
||||||
|
console.log(`Encontrados ${dbPayments.length} pagamentos com valor_pago no SQL.`);
|
||||||
|
|
||||||
|
const jsonPath = 'C:/Users/Professor/Downloads/remix_-edumanager---sistema-de-gestão-escolar-para-porteiner/school_data.json';
|
||||||
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
dbPayments.forEach(dbP => {
|
||||||
|
const pIdx = data.payments.findIndex(p => p.asaasPaymentId === dbP.asaas_payment_id);
|
||||||
|
if (pIdx !== -1) {
|
||||||
|
data.payments[pIdx].valor_pago = Number(dbP.valor_pago);
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
data.lastUpdated = new Date().toISOString();
|
||||||
|
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
console.log(`--- Sincronização Concluída ---`);
|
||||||
|
console.log(`JSON atualizado com ${updatedCount} valores reais de pagamento.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro na sincronização:', err);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
massSync();
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function debug() {
|
||||||
|
const studentId = '311709fb-68ab-4168-8684-887b5ec2d731';
|
||||||
|
|
||||||
|
console.log('--- SQL DATA (Napoleão) ---');
|
||||||
|
const { rows } = await pool.query('SELECT asaas_payment_id, valor, amount_original, valor_pago, vencimento, status FROM alunos_cobrancas WHERE aluno_id = $1', [studentId]);
|
||||||
|
console.table(rows);
|
||||||
|
|
||||||
|
console.log('--- JSON DATA (Napoleão) ---');
|
||||||
|
const jsonPath = 'C:/Users/Professor/Downloads/remix_-edumanager---sistema-de-gestão-escolar-para-porteiner/school_data.json';
|
||||||
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
|
const payments = data.payments.filter(p => p.studentId === studentId);
|
||||||
|
console.table(payments.map(p => ({
|
||||||
|
asaasPaymentId: p.asaasPaymentId,
|
||||||
|
amount: p.amount,
|
||||||
|
discount: p.discount,
|
||||||
|
dueDate: p.dueDate,
|
||||||
|
status: p.status
|
||||||
|
})));
|
||||||
|
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug();
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import pg from 'pg';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const pool = new pg.Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL || 'postgresql://edumanager:edumanager2024@localhost:5432/edumanager'
|
|
||||||
});
|
|
||||||
|
|
||||||
async function debug() {
|
|
||||||
try {
|
|
||||||
// 1. Buscar todos os alunos
|
|
||||||
const alunos = await pool.query('SELECT id, nome FROM alunos LIMIT 20');
|
|
||||||
console.log('\n=== ALUNOS CADASTRADOS ===');
|
|
||||||
alunos.rows.forEach(a => console.log(` ${a.id} | ${a.nome}`));
|
|
||||||
|
|
||||||
// 2. Buscar TODAS as cobranças do SQL
|
|
||||||
const cobrancas = await pool.query(`
|
|
||||||
SELECT asaas_payment_id, aluno_id, status,
|
|
||||||
TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento,
|
|
||||||
valor, amount_original, discount, description, type,
|
|
||||||
TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento,
|
|
||||||
transaction_receipt_url
|
|
||||||
FROM alunos_cobrancas
|
|
||||||
ORDER BY aluno_id, vencimento ASC
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(`\n=== COBRANÇAS NO SQL (${cobrancas.rows.length} total) ===`);
|
|
||||||
const byStudent = {};
|
|
||||||
cobrancas.rows.forEach(c => {
|
|
||||||
if (!byStudent[c.aluno_id]) byStudent[c.aluno_id] = [];
|
|
||||||
byStudent[c.aluno_id].push(c);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [alunoId, payments] of Object.entries(byStudent)) {
|
|
||||||
const aluno = alunos.rows.find(a => a.id === alunoId);
|
|
||||||
console.log(`\n 📌 ${aluno?.nome || alunoId} (${payments.length} cobranças):`);
|
|
||||||
payments.forEach(p => {
|
|
||||||
const status = p.status?.toUpperCase();
|
|
||||||
const icon = status === 'PAGO' ? '✅' : status === 'PENDENTE' ? '⏳' : status === 'ATRASADO' ? '🔴' : '❓';
|
|
||||||
console.log(` ${icon} ${p.asaas_payment_id} | ${p.vencimento} | R$${Number(p.valor).toFixed(2)} | Status: ${p.status} | Pago em: ${p.data_pagamento || '-'} | Desc: ${p.description || '-'} | amount_original: ${p.amount_original || '-'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Buscar do JSON
|
|
||||||
const dataPath = path.join(__dirname, '..', 'data', 'school_data.json');
|
|
||||||
if (fs.existsSync(dataPath)) {
|
|
||||||
const data = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
|
|
||||||
const jsonPayments = data.payments || [];
|
|
||||||
console.log(`\n=== COBRANÇAS NO JSON (${jsonPayments.length} total) ===`);
|
|
||||||
|
|
||||||
const jsonByStudent = {};
|
|
||||||
jsonPayments.forEach(p => {
|
|
||||||
if (!jsonByStudent[p.studentId]) jsonByStudent[p.studentId] = [];
|
|
||||||
jsonByStudent[p.studentId].push(p);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [studentId, payments] of Object.entries(jsonByStudent)) {
|
|
||||||
const aluno = alunos.rows.find(a => a.id === studentId);
|
|
||||||
console.log(`\n 📌 ${aluno?.nome || studentId} (${payments.length} cobranças no JSON):`);
|
|
||||||
payments.forEach(p => {
|
|
||||||
const inSql = cobrancas.rows.find(c => c.asaas_payment_id === p.asaasPaymentId);
|
|
||||||
console.log(` ${inSql ? '🔗' : '⚠️'} ${p.asaasPaymentId || 'SEM_ID'} | ${p.dueDate} | R$${p.amount} | Status: ${p.status} | Desc: ${p.description || '-'} | discount: ${p.discount || 0} | ${inSql ? 'Existe no SQL' : 'SÓ NO JSON!'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Verificar cobranças que existem no SQL mas NÃO no JSON
|
|
||||||
console.log('\n=== COBRANÇAS QUE EXISTEM NO SQL MAS NÃO NO JSON ===');
|
|
||||||
let orphanCount = 0;
|
|
||||||
cobrancas.rows.forEach(c => {
|
|
||||||
const inJson = jsonPayments.find(p => p.asaasPaymentId === c.asaas_payment_id);
|
|
||||||
if (!inJson) {
|
|
||||||
orphanCount++;
|
|
||||||
console.log(` ⚠️ ${c.asaas_payment_id} | Aluno: ${c.aluno_id} | ${c.vencimento} | R$${Number(c.valor).toFixed(2)} | Status: ${c.status}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (orphanCount === 0) console.log(' ✅ Nenhuma — tudo sincronizado.');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro:', err);
|
|
||||||
} finally {
|
|
||||||
await pool.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug();
|
|
||||||
|
|
@ -802,10 +802,16 @@ app.post('/api/webhook_asaas', async (req, res) => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'PAYMENT_UPDATED':
|
case 'PAYMENT_UPDATED':
|
||||||
|
updateData = {
|
||||||
|
valor: payload.payment.value,
|
||||||
|
vencimento: payload.payment.dueDate,
|
||||||
|
link_boleto: payload.payment.bankSlipUrl || payload.payment.link || null
|
||||||
|
};
|
||||||
|
|
||||||
// Alerta no Sino (Admin)
|
// Alerta no Sino (Admin)
|
||||||
createAdminNotification(
|
createAdminNotification(
|
||||||
'📝 Cobrança Alterada',
|
'📝 Cobrança Alterada',
|
||||||
`A cobrança de ${targetName} foi atualizada no Asaas.`,
|
`A cobrança de ${targetName} foi atualizada no Asaas para R$ ${Number(payload.payment.value).toFixed(2)}.`,
|
||||||
{ type: 'finance', status: 'updated', paymentId: asaasPaymentId }
|
{ type: 'finance', status: 'updated', paymentId: asaasPaymentId }
|
||||||
);
|
);
|
||||||
if (payload.event === 'PAYMENT_UPDATED') sendEvolutionMessage(asaasPaymentId, 'PAYMENT_UPDATED');
|
if (payload.event === 'PAYMENT_UPDATED') sendEvolutionMessage(asaasPaymentId, 'PAYMENT_UPDATED');
|
||||||
|
|
@ -866,6 +872,7 @@ app.post('/api/webhook_asaas', async (req, res) => {
|
||||||
...p,
|
...p,
|
||||||
status: newStatus,
|
status: newStatus,
|
||||||
amount: shouldUpdateAmount ? updateData.valor : p.amount,
|
amount: shouldUpdateAmount ? updateData.valor : p.amount,
|
||||||
|
valor_pago: updateData.valor_pago || p.valor_pago || 0,
|
||||||
dueDate: updateData.vencimento || p.dueDate,
|
dueDate: updateData.vencimento || p.dueDate,
|
||||||
paidDate: updateData.data_pagamento || p.paidDate
|
paidDate: updateData.data_pagamento || p.paidDate
|
||||||
};
|
};
|
||||||
|
|
@ -1578,40 +1585,49 @@ async function syncPaymentsWithAsaasAPI() {
|
||||||
|
|
||||||
const valorNum = Number(payment.value);
|
const valorNum = Number(payment.value);
|
||||||
|
|
||||||
// A. Atualiza SQL (Silencioso)
|
// A. Atualiza SQL (Prioridade Máxima)
|
||||||
|
const receivedValue = (internalStatus === 'paid') ? valorNum : 0;
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO alunos_cobrancas (asaas_payment_id, valor, vencimento, status, data_pagamento)
|
INSERT INTO alunos_cobrancas (asaas_payment_id, valor, vencimento, status, data_pagamento, valor_pago, amount_original)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6, $2)
|
||||||
ON CONFLICT (asaas_payment_id) DO UPDATE SET status = EXCLUDED.status, data_pagamento = EXCLUDED.data_pagamento
|
ON CONFLICT (asaas_payment_id) DO UPDATE SET
|
||||||
`, [payment.id, valorNum, payment.dueDate, internalStatus, payment.confirmedDate || payment.paymentDate]).catch(() => {});
|
status = EXCLUDED.status,
|
||||||
|
data_pagamento = EXCLUDED.data_pagamento,
|
||||||
|
valor_pago = GREATEST(alunos_cobrancas.valor_pago, EXCLUDED.valor_pago),
|
||||||
|
valor = GREATEST(alunos_cobrancas.valor, EXCLUDED.valor)
|
||||||
|
`, [payment.id, valorNum, payment.dueDate, internalStatus, payment.confirmedDate || payment.paymentDate, receivedValue]).catch(() => {});
|
||||||
|
|
||||||
// B. Atualiza JSON
|
// B. Atualiza JSON
|
||||||
const pIdx = appData.payments.findIndex(p => p.asaasPaymentId === payment.id);
|
const pIdx = appData.payments.findIndex(p => p.asaasPaymentId === payment.id);
|
||||||
if (pIdx !== -1) {
|
if (pIdx !== -1) {
|
||||||
const newStatus = jsonStatusMap[internalStatus];
|
const newStatus = jsonStatusMap[internalStatus];
|
||||||
|
const p = appData.payments[pIdx];
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
if (appData.payments[pIdx].status !== newStatus) {
|
if (p.status !== newStatus) {
|
||||||
appData.payments[pIdx].status = newStatus;
|
p.status = newStatus;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Bugfix Crítico]: Não sobrescrever o valor BRUTO com o valor LÍQUIDO (descontado) do Asaas
|
// [Bugfix Crítico]: Não sobrescrever o valor BRUTO com o valor LÍQUIDO (descontado) do Asaas
|
||||||
const currentAmount = Number(appData.payments[pIdx].amount || 0);
|
const currentAmount = Number(p.amount || 0);
|
||||||
const currentDiscount = Number(appData.payments[pIdx].discount || 0);
|
const currentDiscount = Number(p.discount || 0);
|
||||||
|
|
||||||
// Se o valor vindo do Asaas for menor que o atual E a diferença bater com o desconto, ignoramos o update do valor
|
|
||||||
// para preservar o valor bruto original no display do portal/gerenciador.
|
|
||||||
const isNetValueOverwrite = valorNum < currentAmount && Math.abs((currentAmount - currentDiscount) - valorNum) < 0.01;
|
const isNetValueOverwrite = valorNum < currentAmount && Math.abs((currentAmount - currentDiscount) - valorNum) < 0.01;
|
||||||
|
|
||||||
if (appData.payments[pIdx].amount !== valorNum && !isNetValueOverwrite) {
|
if (p.amount !== valorNum && !isNetValueOverwrite) {
|
||||||
appData.payments[pIdx].amount = valorNum;
|
p.amount = valorNum;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar valor_pago ao JSON para o Manager ler
|
||||||
|
if (receivedValue > 0 && Number(p.valor_pago || 0) !== receivedValue) {
|
||||||
|
p.valor_pago = receivedValue;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPaidDate = payment.confirmedDate || payment.paymentDate;
|
const newPaidDate = payment.confirmedDate || payment.paymentDate;
|
||||||
if (newPaidDate && appData.payments[pIdx].paidDate !== newPaidDate) {
|
if (newPaidDate && p.paidDate !== newPaidDate) {
|
||||||
appData.payments[pIdx].paidDate = newPaidDate;
|
p.paidDate = newPaidDate;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1648,9 +1664,19 @@ async function syncRelationalToJsonPayments() {
|
||||||
statusStr === 'atrasado' ? 'overdue' :
|
statusStr === 'atrasado' ? 'overdue' :
|
||||||
statusStr === 'cancelado' ? 'cancelled' : 'pending';
|
statusStr === 'cancelado' ? 'cancelled' : 'pending';
|
||||||
|
|
||||||
if (p.status !== newStatus) {
|
const hasChanges = p.status !== newStatus ||
|
||||||
|
Number(p.valor_pago || 0) !== Number(match.valor_pago || 0) ||
|
||||||
|
Number(p.amount || 0) !== Math.max(Number(match.amount_original || 0), Number(match.valor || 0));
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
return { ...p, status: newStatus, paidDate: match.data_pagamento || p.paidDate };
|
return {
|
||||||
|
...p,
|
||||||
|
status: newStatus,
|
||||||
|
paidDate: match.data_pagamento || p.paidDate,
|
||||||
|
valor_pago: Number(match.valor_pago || 0),
|
||||||
|
amount: Math.max(Number(match.amount_original || 0), Number(match.valor || 0))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
|
|
|
||||||
|
|
@ -283,18 +283,22 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes(normalizedStatus);
|
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes(normalizedStatus);
|
||||||
const valorPagoNoSQL = Number(db.valor_pago || 0);
|
const valorPagoNoSQL = Number(db.valor_pago || 0);
|
||||||
|
|
||||||
// Tentamos pegar o maior valor disponível como o Bruto
|
// [NOVA LÓGICA]: Pegar o MAIOR valor entre todas as fontes para garantir que seja o BRUTO
|
||||||
let amountBruto = Number(db.amount_original) || Number(jsonP.amount) || Number(db.valor) || 0;
|
let amountBruto = Math.max(
|
||||||
|
Number(db.amount_original || 0),
|
||||||
|
Number(db.valor || 0),
|
||||||
|
Number(jsonP.amount || 0)
|
||||||
|
);
|
||||||
|
|
||||||
// Se está pago e o amountBruto parece ser o líquido (igual ao pago), recomponha
|
// Se o valor bruto encontrado é igual ao que foi pago, e existe desconto,
|
||||||
if (isPaid && discount > 0 && amountBruto > 0 && (amountBruto === valorPagoNoSQL || (valorPagoNoSQL === 0 && amountBruto === Number(db.valor)))) {
|
// então o que encontramos era na verdade o valor líquido. Recuperamos o bruto somando o desconto.
|
||||||
// Se o SQL tem o valor_pago correto, e o amountBruto é igual a ele, some o desconto
|
if (isPaid && discount > 0 && amountBruto > 0) {
|
||||||
if (amountBruto === valorPagoNoSQL) {
|
if (valorPagoNoSQL > 0 && amountBruto <= valorPagoNoSQL) {
|
||||||
amountBruto += discount;
|
amountBruto = valorPagoNoSQL + discount;
|
||||||
} else if (valorPagoNoSQL === 0 && amountBruto === Number(db.valor)) {
|
} else if (amountBruto < (amountBruto + discount) && amountBruto === (jsonP.amount || 0)) {
|
||||||
// Fallback para quando valor_pago ainda não foi preenchido (primeira vez)
|
// Se veio do JSON e parece ser o líquido
|
||||||
amountBruto += discount;
|
amountBruto = Number(jsonP.amount) + discount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalPayments.push({
|
finalPayments.push({
|
||||||
|
|
@ -304,7 +308,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null,
|
asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null,
|
||||||
amount: amountBruto,
|
amount: amountBruto,
|
||||||
discount: discount,
|
discount: discount,
|
||||||
valor_pago: valorPagoNoSQL || (isPaid ? Number(db.valor) : 0),
|
valor_pago: valorPagoNoSQL > 0 ? valorPagoNoSQL : (isPaid ? (amountBruto - discount) : 0),
|
||||||
dueDate: db.vencimento || jsonP.dueDate,
|
dueDate: db.vencimento || jsonP.dueDate,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
paidDate: db.data_pagamento || jsonP.paidDate || null,
|
paidDate: db.data_pagamento || jsonP.paidDate || null,
|
||||||
|
|
|
||||||
|
|
@ -152,56 +152,28 @@ export default function Financeiro() {
|
||||||
return (boleto as any)?.link_boleto || null;
|
return (boleto as any)?.link_boleto || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEffectiveValue = (payment: Payment) => {
|
const getDisplayValue = (payment: Payment) => {
|
||||||
const baseAmount = payment.amount || 0;
|
|
||||||
const discount = payment.discount || 0;
|
|
||||||
const netAmount = baseAmount - discount;
|
|
||||||
const status = normalizeStatus(payment);
|
const status = normalizeStatus(payment);
|
||||||
|
const isPaid = status === 'paid';
|
||||||
|
const valorPago = (payment as any).valor_pago ? Number((payment as any).valor_pago) : 0;
|
||||||
|
|
||||||
// Try to find matching boleto from Supabase sync
|
// Se está pago e temos o valor real no banco, mostramos ele SEM CÁLCULOS
|
||||||
const asaasId = payment.asaasPaymentId || (payment as any).asaas_payment_id;
|
if (isPaid && valorPago > 0) return valorPago;
|
||||||
let boleto = null;
|
|
||||||
|
|
||||||
if (asaasId) {
|
// Se está pago mas o banco ainda não sincronizou o valor_pago (fallback)
|
||||||
boleto = boletos.find(b => (b as any).asaas_payment_id === asaasId);
|
if (isPaid) return payment.amount - (payment.discount || 0);
|
||||||
}
|
|
||||||
|
|
||||||
if (!boleto) {
|
// Se está pendente ou atrasado, mostramos o que falta pagar (Líquido esperado)
|
||||||
// Fallback: Match by due date and base amount (allowing for interest/fines)
|
return payment.amount - (payment.discount || 0);
|
||||||
boleto = boletos.find(b => {
|
|
||||||
const bVenc = (b as any).vencimento;
|
|
||||||
const bVal = Number((b as any).valor);
|
|
||||||
|
|
||||||
// Exact date match
|
|
||||||
if (bVenc === payment.dueDate) {
|
|
||||||
// If value is exactly base or exactly net
|
|
||||||
if (Math.abs(bVal - baseAmount) < 1 || Math.abs(bVal - netAmount) < 1) return true;
|
|
||||||
// If it's overdue, the boleto value will be HIGHER than baseAmount
|
|
||||||
if (status === 'overdue' && bVal > netAmount) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a boleto and it is overdue or paid, use current Asaas value
|
|
||||||
if (boleto && (boleto as any).valor) {
|
|
||||||
const bValue = Number((boleto as any).valor);
|
|
||||||
if (status === 'overdue' || status === 'paid') {
|
|
||||||
return bValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: use the discounted base value (net amount)
|
|
||||||
return netAmount;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalPending = payments
|
const totalPending = payments
|
||||||
.filter(p => isPending(p))
|
.filter(p => isPending(p))
|
||||||
.reduce((s, p) => s + getEffectiveValue(p), 0);
|
.reduce((s, p) => s + getDisplayValue(p), 0);
|
||||||
|
|
||||||
const totalPaid = payments
|
const totalPaid = payments
|
||||||
.filter(p => isPaid(p))
|
.filter(p => isPaid(p))
|
||||||
.reduce((s, p) => s + getEffectiveValue(p), 0);
|
.reduce((s, p) => s + getDisplayValue(p), 0);
|
||||||
|
|
||||||
const filters: { key: FilterType; label: string }[] = [
|
const filters: { key: FilterType; label: string }[] = [
|
||||||
{ key: 'all', label: 'Todos' },
|
{ key: 'all', label: 'Todos' },
|
||||||
|
|
@ -348,7 +320,7 @@ export default function Financeiro() {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: normalizeStatus(payment) === 'overdue' ? 'var(--color-danger)' : 'var(--color-primary-light)'
|
color: normalizeStatus(payment) === 'overdue' ? 'var(--color-danger)' : 'var(--color-primary-light)'
|
||||||
}}>
|
}}>
|
||||||
{formatCurrency(getEffectiveValue(payment))}
|
{formatCurrency(getDisplayValue(payment))}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Status">{getStatusBadge(payment)}</td>
|
<td data-label="Status">{getStatusBadge(payment)}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -415,7 +387,7 @@ export default function Financeiro() {
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<span style={{ color: 'var(--color-text-secondary)' }}>Valor Pago:</span>
|
<span style={{ color: 'var(--color-text-secondary)' }}>Valor Pago:</span>
|
||||||
<span style={{ fontWeight: 600 }}>{formatCurrency(receiptPayment.amount - (receiptPayment.discount || 0))}</span>
|
<span style={{ fontWeight: 600 }}>{formatCurrency(getEffectiveValue(receiptPayment))}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<span style={{ color: 'var(--color-text-secondary)' }}>Data de Vencimento:</span>
|
<span style={{ color: 'var(--color-text-secondary)' }}>Data de Vencimento:</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue