fix(finance): implement gross amount recovery and protection against net value overwrites in portal and manager
This commit is contained in:
parent
cf1ad968ca
commit
8a42db3e58
|
|
@ -1180,8 +1180,26 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-5 text-slate-600 text-sm">{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
<td className="px-4 py-5 text-slate-600 text-sm">{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
||||||
<td className="px-4 py-5">
|
<td className="px-4 py-5">
|
||||||
<div className="font-black text-slate-900 text-sm">R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
<div className="font-black text-slate-900 text-sm">
|
||||||
{!!payment.discount && payment.discount > 0 && <div className="text-[10px] text-emerald-600 font-bold">- R$ {payment.discount.toFixed(2)}</div>}
|
R$ {(() => {
|
||||||
|
const amt = Number(payment.amount);
|
||||||
|
const disc = Number(payment.discount || 0);
|
||||||
|
const status = (payment.status || '').toLowerCase();
|
||||||
|
const isPaid = status === 'paid' || status === 'pago' || status === 'received' || status === 'confirmed';
|
||||||
|
|
||||||
|
// Se está pago e temos desconto, e o valor original é maior ou não existe, tentamos recuperar o bruto
|
||||||
|
// No manager, as cobranças do SQL (filteredPayments) costumam ter amount_original
|
||||||
|
const amtOrig = (payment as any).amount_original ? Number((payment as any).amount_original) : 0;
|
||||||
|
|
||||||
|
if (isPaid && disc > 0) {
|
||||||
|
if (amtOrig > amt) return amtOrig.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||||
|
// Se não tem amtOrig mas o valor atual é suspeito de ser líquido (não implementado aqui para evitar falso positivo,
|
||||||
|
// mas o sync de backend já deve ter protegido o amount_original agora)
|
||||||
|
}
|
||||||
|
return amt.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{!!payment.discount && payment.discount > 0 && <div className="text-[10px] text-emerald-600 font-bold">- R$ {payment.discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-5">{getStatusBadge(payment)}</td>
|
<td className="px-4 py-5">{getStatusBadge(payment)}</td>
|
||||||
<td className="px-4 py-5">
|
<td className="px-4 py-5">
|
||||||
|
|
|
||||||
|
|
@ -1572,8 +1572,15 @@ async function syncPaymentsWithAsaasAPI() {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SEMPRE atualiza o valor e a data para garantir fidelidade ao Asaas
|
// [Bugfix Crítico]: Não sobrescrever o valor BRUTO com o valor LÍQUIDO (descontado) do Asaas
|
||||||
if (appData.payments[pIdx].amount !== valorNum) {
|
const currentAmount = Number(appData.payments[pIdx].amount || 0);
|
||||||
|
const currentDiscount = Number(appData.payments[pIdx].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;
|
||||||
|
|
||||||
|
if (appData.payments[pIdx].amount !== valorNum && !isNetValueOverwrite) {
|
||||||
appData.payments[pIdx].amount = valorNum;
|
appData.payments[pIdx].amount = valorNum;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -572,7 +572,7 @@ export async function syncJsonToRelationalTables() {
|
||||||
total_installments = COALESCE(EXCLUDED.total_installments, alunos_cobrancas.total_installments),
|
total_installments = COALESCE(EXCLUDED.total_installments, alunos_cobrancas.total_installments),
|
||||||
contract_id = COALESCE(EXCLUDED.contract_id, alunos_cobrancas.contract_id),
|
contract_id = COALESCE(EXCLUDED.contract_id, alunos_cobrancas.contract_id),
|
||||||
asaas_payment_url = COALESCE(EXCLUDED.asaas_payment_url, alunos_cobrancas.asaas_payment_url),
|
asaas_payment_url = COALESCE(EXCLUDED.asaas_payment_url, alunos_cobrancas.asaas_payment_url),
|
||||||
amount_original = COALESCE(EXCLUDED.amount_original, alunos_cobrancas.amount_original),
|
amount_original = GREATEST(COALESCE(alunos_cobrancas.amount_original, 0), EXCLUDED.amount_original),
|
||||||
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento)`,
|
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento)`,
|
||||||
[
|
[
|
||||||
p.studentId,
|
p.studentId,
|
||||||
|
|
|
||||||
|
|
@ -278,16 +278,24 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Bugfix Crítico]: O webhook do Asaas sobrescreveu o JSON e o banco com o valor LÍQUIDO.
|
// [Bugfix Crítico]: Recuperar valor bruto se o Asaas/Webhook salvou apenas o líquido
|
||||||
// Ou seja, jsonP.amount e db.valor estão iguais (ex: 150), mas o correto é 170.
|
|
||||||
// Se detectarmos essa corrupção (amountOriginal == db.valor) e houver desconto, restauramos o valor bruto.
|
|
||||||
let amountOriginal = jsonP.amount !== undefined ? Number(jsonP.amount) : (Number(db.amount_original) || Number(db.valor) || 0);
|
let amountOriginal = jsonP.amount !== undefined ? Number(jsonP.amount) : (Number(db.amount_original) || Number(db.valor) || 0);
|
||||||
const discount = jsonP.discount !== undefined ? Number(jsonP.discount) : (Number(db.discount) || 0);
|
const discount = jsonP.discount !== undefined ? Number(jsonP.discount) : (Number(db.discount) || 0);
|
||||||
|
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes(normalizedStatus);
|
||||||
|
|
||||||
// Aplica a recuperação matemática INDEPENDENTE de onde veio (SQL ou JSON)
|
// Se está pago e o valor que temos parece ser o líquido (igual ao do banco que o webhook sobrescreve)
|
||||||
if (amountOriginal > 0 && amountOriginal === Number(db.valor) && discount > 0) {
|
// e temos um desconto registrado, restauramos o bruto para o display.
|
||||||
amountOriginal += discount;
|
if (isPaid && discount > 0 && amountOriginal > 0 && (amountOriginal === Number(db.valor) || amountOriginal < (Number(db.valor) + discount))) {
|
||||||
|
// Se amountOriginal já é o bruto (maior que db.valor), o GREATEST ou a lógica preserva.
|
||||||
|
// Se for igual, somamos o desconto.
|
||||||
|
if (amountOriginal === Number(db.valor)) {
|
||||||
|
amountOriginal += discount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Garantir que sempre usamos o maior valor conhecido como bruto
|
||||||
|
const dbAmountOrig = Number(db.amount_original || 0);
|
||||||
|
if (dbAmountOrig > amountOriginal) amountOriginal = dbAmountOrig;
|
||||||
|
|
||||||
finalPayments.push({
|
finalPayments.push({
|
||||||
id: jsonP.id || asaasId,
|
id: jsonP.id || asaasId,
|
||||||
|
|
@ -338,7 +346,32 @@ app.get('/api/portal/boletos', authMiddleware, async (req, res) => {
|
||||||
`SELECT *, TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento, TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC`,
|
`SELECT *, TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento, TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC`,
|
||||||
[req.user.studentId]
|
[req.user.studentId]
|
||||||
);
|
);
|
||||||
res.json({ boletos: rows || [] });
|
|
||||||
|
// [Bugfix Crítico]: Recuperar valor bruto original se o banco estiver com o valor líquido
|
||||||
|
const boletos = (rows || []).map(b => {
|
||||||
|
let valor = Number(b.valor);
|
||||||
|
const discount = Number(b.discount || 0);
|
||||||
|
const amountOriginal = Number(b.amount_original || 0);
|
||||||
|
const status = (b.status || '').toLowerCase();
|
||||||
|
const isPaid = ['paid', 'pago', 'received', 'confirmed', 'recebido'].includes(status);
|
||||||
|
|
||||||
|
// Prioridade 1: amount_original explícito
|
||||||
|
if (amountOriginal > valor) {
|
||||||
|
valor = amountOriginal;
|
||||||
|
}
|
||||||
|
// Prioridade 2: Recomposição matemática se estiver pago e valor == líquido
|
||||||
|
else if (isPaid && discount > 0 && valor > 0) {
|
||||||
|
// Se o valor no banco é exatamente o que o webhook salvaria (líquido), recompomos
|
||||||
|
valor += discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
valor: valor
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ boletos });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Boletos error:', err);
|
console.error('Boletos error:', err);
|
||||||
res.json({ boletos: [] });
|
res.json({ boletos: [] });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue