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 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">
|
||||
<div className="font-black text-slate-900 text-sm">R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
||||
{!!payment.discount && payment.discount > 0 && <div className="text-[10px] text-emerald-600 font-bold">- R$ {payment.discount.toFixed(2)}</div>}
|
||||
<div className="font-black text-slate-900 text-sm">
|
||||
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 className="px-4 py-5">{getStatusBadge(payment)}</td>
|
||||
<td className="px-4 py-5">
|
||||
|
|
|
|||
|
|
@ -1572,8 +1572,15 @@ async function syncPaymentsWithAsaasAPI() {
|
|||
changed = true;
|
||||
}
|
||||
|
||||
// SEMPRE atualiza o valor e a data para garantir fidelidade ao Asaas
|
||||
if (appData.payments[pIdx].amount !== valorNum) {
|
||||
// [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 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;
|
||||
changed = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ export async function syncJsonToRelationalTables() {
|
|||
total_installments = COALESCE(EXCLUDED.total_installments, alunos_cobrancas.total_installments),
|
||||
contract_id = COALESCE(EXCLUDED.contract_id, alunos_cobrancas.contract_id),
|
||||
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)`,
|
||||
[
|
||||
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.
|
||||
// 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.
|
||||
// [Bugfix Crítico]: Recuperar valor bruto se o Asaas/Webhook salvou apenas o líquido
|
||||
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 isPaid = ['paid', 'pago', 'received', 'confirmed'].includes(normalizedStatus);
|
||||
|
||||
// Aplica a recuperação matemática INDEPENDENTE de onde veio (SQL ou JSON)
|
||||
if (amountOriginal > 0 && amountOriginal === Number(db.valor) && discount > 0) {
|
||||
amountOriginal += discount;
|
||||
// Se está pago e o valor que temos parece ser o líquido (igual ao do banco que o webhook sobrescreve)
|
||||
// e temos um desconto registrado, restauramos o bruto para o display.
|
||||
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({
|
||||
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`,
|
||||
[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) {
|
||||
console.error('Boletos error:', err);
|
||||
res.json({ boletos: [] });
|
||||
|
|
|
|||
Loading…
Reference in New Issue