fix: align overdue interest, fine and discount expiration calculations with Asaas rules
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m5s Details

This commit is contained in:
Sidney 2026-06-10 09:30:40 -03:00
parent be7ce85a2f
commit 3cb3d599b5
3 changed files with 66 additions and 26 deletions

View File

@ -1188,21 +1188,36 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((payment.status || '').toLowerCase());
const valorPago = Number((payment as any).valor_pago || 0);
const discount = Number(payment.discount || 0);
let additions = 0;
const isOverdue = ['overdue', 'atrasado'].includes((payment.status || '').toLowerCase());
// Para parcelas vencidas, o desconto perde a validade
const activeDiscount = (!isPaid && isOverdue) ? 0 : discount;
if (!isPaid && isOverdue && payment.dueDate) {
const dd = new Date(payment.dueDate + 'T00:00:00');
const tt = new Date(); tt.setHours(0,0,0,0);
if (tt > dd) {
const baseA = payment.amount - discount;
// Regra de Fins de Semana (Banco/Asaas): Vencimento no sábado ou domingo posterga cobrança de encargos para segunda
let vencimentoEfetivo = new Date(dd);
const diaSemana = dd.getDay(); // 0 = Domingo, 6 = Sábado
if (diaSemana === 6) {
vencimentoEfetivo.setDate(dd.getDate() + 2);
} else if (diaSemana === 0) {
vencimentoEfetivo.setDate(dd.getDate() + 1);
}
if (tt > vencimentoEfetivo) {
// Vencido perde o desconto. Base de cálculo é o valor bruto original (payment.amount).
const baseA = payment.amount;
const dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
const student = data.students.find(s => s.id === payment.studentId);
const studentClass = data.classes.find(c => c.id === student?.classId);
const course = data.courses.find(c => c.id === studentClass?.courseId);
const pLateFee = Number(payment.lateFee) || Number(course?.finePercentage) || 2;
const pInterest = Number(payment.interest) || Number(course?.interestPercentage) || 1;
const pLateFee = Number(payment.lateFee ?? course?.finePercentage ?? 0);
const pInterest = Number(payment.interest ?? course?.interestPercentage ?? 0);
const fineAmt = baseA * (pLateFee / 100);
const intAmt = baseA * ((pInterest / 30) / 100) * dDays;
@ -1223,9 +1238,9 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
<div className="font-bold text-slate-700 text-sm">
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</div>
{discount > 0 && (
{activeDiscount > 0 && (
<div className="text-[10px] text-emerald-600 font-bold">
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
- R$ {activeDiscount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</div>
)}
{additions > 0 && (
@ -1258,7 +1273,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
{(payment.installmentId || payment.asaasInstallmentId) && (
<button
onClick={() => executePrintCarne(payment.installmentId || payment.asaasInstallmentId)}
className="px-2 py-1 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold hover:bg-indigo-100 inline-flex items-center gap-1"
className="px-2 py-1 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold hover:bg-indigo-100 transition-colors inline-flex items-center gap-1"
title="Imprimir Carnê Completo"
>
<Printer size={11} /> Carnê
@ -1315,18 +1330,33 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
let additions = 0;
const isOverdue = ['overdue', 'atrasado'].includes((payment.status || '').toLowerCase());
// Para parcelas vencidas, o desconto perde a validade
const activeDiscount = (!isPaid && isOverdue) ? 0 : discount;
if (!isPaid && isOverdue && payment.dueDate) {
const dd = new Date(payment.dueDate + 'T00:00:00');
const tt = new Date(); tt.setHours(0,0,0,0);
if (tt > dd) {
const baseA = payment.amount - discount;
// Regra de Fins de Semana (Banco/Asaas): Vencimento no sábado ou domingo posterga cobrança de encargos para segunda
let vencimentoEfetivo = new Date(dd);
const diaSemana = dd.getDay(); // 0 = Domingo, 6 = Sábado
if (diaSemana === 6) {
vencimentoEfetivo.setDate(dd.getDate() + 2);
} else if (diaSemana === 0) {
vencimentoEfetivo.setDate(dd.getDate() + 1);
}
if (tt > vencimentoEfetivo) {
// Vencido perde o desconto. Base de cálculo é o valor bruto original (payment.amount).
const baseA = payment.amount;
const dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
const student = data.students.find(s => s.id === payment.studentId);
const studentClass = data.classes.find(c => c.id === student?.classId);
const course = data.courses.find(c => c.id === studentClass?.courseId);
const pLateFee = Number(payment.lateFee) || Number(course?.finePercentage) || 2;
const pInterest = Number(payment.interest) || Number(course?.interestPercentage) || 1;
const pLateFee = Number(payment.lateFee ?? course?.finePercentage ?? 0);
const pInterest = Number(payment.interest ?? course?.interestPercentage ?? 0);
const fineAmt = baseA * (pLateFee / 100);
const intAmt = baseA * ((pInterest / 30) / 100) * dDays;
@ -1347,9 +1377,9 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
<div className="font-black text-slate-900 text-sm">
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</div>
{discount > 0 && (
{activeDiscount > 0 && (
<div className="text-[10px] text-emerald-600 font-bold">
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
- R$ {activeDiscount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</div>
)}
{additions > 0 && (

View File

@ -318,8 +318,8 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
}
// Buscar configurações de juros do curso do aluno
let defaultLateFee = 2;
let defaultInterest = 1;
let defaultLateFee = 0;
let defaultInterest = 0;
try {
const { rows: studentRows } = await pool.query('SELECT turma_id FROM alunos WHERE id = $1', [req.user.studentId]);
if (studentRows.length > 0 && studentRows[0].turma_id) {
@ -327,8 +327,8 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
if (tRows.length > 0 && tRows[0].curso_id) {
const { rows: cRows } = await pool.query('SELECT multa_percentual, juros_percentual FROM cursos WHERE id = $1', [tRows[0].curso_id]);
if (cRows.length > 0) {
defaultLateFee = Number(cRows[0].multa_percentual) || 2;
defaultInterest = Number(cRows[0].juros_percentual) || 1;
defaultLateFee = cRows[0].multa_percentual !== null ? Number(cRows[0].multa_percentual) : 0;
defaultInterest = cRows[0].juros_percentual !== null ? Number(cRows[0].juros_percentual) : 0;
}
}
}

View File

@ -163,22 +163,32 @@ export default function Financeiro() {
// Se está pago mas o banco ainda não sincronizou o valor_pago (fallback)
if (isPaid) return payment.amount - (payment.discount || 0);
const baseAmount = payment.amount - (payment.discount || 0);
// Simulação Matemática de Atraso
if (status === 'overdue' && payment.dueDate) {
const dd = new Date(payment.dueDate + 'T00:00:00');
const tt = new Date(); tt.setHours(0,0,0,0);
if (tt > dd) {
// Regra de Fins de Semana (Banco/Asaas): Vencimento no sábado ou domingo posterga cobrança de encargos para segunda
let vencimentoEfetivo = new Date(dd);
const diaSemana = dd.getDay(); // 0 = Domingo, 6 = Sábado
if (diaSemana === 6) {
vencimentoEfetivo.setDate(dd.getDate() + 2);
} else if (diaSemana === 0) {
vencimentoEfetivo.setDate(dd.getDate() + 1);
}
if (tt > vencimentoEfetivo) {
// Vencido perde o desconto. Base de cálculo é o valor bruto original (payment.amount).
const baseAmount = payment.amount;
const dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
const fineAmt = baseAmount * ((Number(payment.lateFee) || 2) / 100);
const intAmt = baseAmount * (((Number(payment.interest) || 1) / 30) / 100) * dDays;
const fineAmt = baseAmount * ((payment.lateFee ?? 0) / 100);
const intAmt = baseAmount * (((payment.interest ?? 0) / 30) / 100) * dDays;
return baseAmount + fineAmt + intAmt;
}
}
// Se está pendente ou atrasado sem juros aplicáveis, mostramos o líquido esperado
return baseAmount;
return payment.amount - (payment.discount || 0);
};
const totalPending = payments
@ -327,8 +337,8 @@ export default function Financeiro() {
<td style={{ fontWeight: 500 }}>
{formatCurrency(payment.amount)}
</td>
<td style={{ color: payment.discount ? 'var(--color-success)' : 'var(--color-text-secondary)', fontSize: '0.8125rem' }}>
{payment.discount ? `- ${formatCurrency(payment.discount)}` : '—'}
<td style={{ color: (payment.discount && normalizeStatus(payment) !== 'overdue') ? 'var(--color-success)' : 'var(--color-text-secondary)', fontSize: '0.8125rem' }}>
{(payment.discount && normalizeStatus(payment) !== 'overdue') ? `- ${formatCurrency(payment.discount)}` : '—'}
</td>
<td style={{
fontWeight: 600,