diff --git a/manager/components/Finance.tsx b/manager/components/Finance.tsx index 18d7a05..6e92478 100644 --- a/manager/components/Finance.tsx +++ b/manager/components/Finance.tsx @@ -1180,8 +1180,26 @@ const Finance: React.FC = ({ data, updateData }) => { {new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })} -
R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
- {!!payment.discount && payment.discount > 0 &&
- R$ {payment.discount.toFixed(2)}
} +
+ 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 }); + })()} +
+ {!!payment.discount && payment.discount > 0 &&
- R$ {payment.discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
} {getStatusBadge(payment)} diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index 22155d0..231c003 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -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; } diff --git a/manager/services/database.js b/manager/services/database.js index 7ad77cc..5ba4bfd 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -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, diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index 6625335..31b10f1 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -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: [] });