fix: align overdue interest, fine and discount expiration calculations with Asaas rules
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m5s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m5s
Details
This commit is contained in:
parent
be7ce85a2f
commit
3cb3d599b5
|
|
@ -1191,18 +1191,33 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
|
|
||||||
let additions = 0;
|
let additions = 0;
|
||||||
const isOverdue = ['overdue', 'atrasado'].includes((payment.status || '').toLowerCase());
|
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) {
|
if (!isPaid && isOverdue && payment.dueDate) {
|
||||||
const dd = new Date(payment.dueDate + 'T00:00:00');
|
const dd = new Date(payment.dueDate + 'T00:00:00');
|
||||||
const tt = new Date(); tt.setHours(0,0,0,0);
|
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 dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
|
||||||
|
|
||||||
const student = data.students.find(s => s.id === payment.studentId);
|
const student = data.students.find(s => s.id === payment.studentId);
|
||||||
const studentClass = data.classes.find(c => c.id === student?.classId);
|
const studentClass = data.classes.find(c => c.id === student?.classId);
|
||||||
const course = data.courses.find(c => c.id === studentClass?.courseId);
|
const course = data.courses.find(c => c.id === studentClass?.courseId);
|
||||||
const pLateFee = Number(payment.lateFee) || Number(course?.finePercentage) || 2;
|
const pLateFee = Number(payment.lateFee ?? course?.finePercentage ?? 0);
|
||||||
const pInterest = Number(payment.interest) || Number(course?.interestPercentage) || 1;
|
const pInterest = Number(payment.interest ?? course?.interestPercentage ?? 0);
|
||||||
|
|
||||||
const fineAmt = baseA * (pLateFee / 100);
|
const fineAmt = baseA * (pLateFee / 100);
|
||||||
const intAmt = baseA * ((pInterest / 30) / 100) * dDays;
|
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">
|
<div className="font-bold text-slate-700 text-sm">
|
||||||
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
</div>
|
</div>
|
||||||
{discount > 0 && (
|
{activeDiscount > 0 && (
|
||||||
<div className="text-[10px] text-emerald-600 font-bold">
|
<div className="text-[10px] text-emerald-600 font-bold">
|
||||||
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
- R$ {activeDiscount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{additions > 0 && (
|
{additions > 0 && (
|
||||||
|
|
@ -1258,7 +1273,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
{(payment.installmentId || payment.asaasInstallmentId) && (
|
{(payment.installmentId || payment.asaasInstallmentId) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => executePrintCarne(payment.installmentId || payment.asaasInstallmentId)}
|
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"
|
title="Imprimir Carnê Completo"
|
||||||
>
|
>
|
||||||
<Printer size={11} /> Carnê
|
<Printer size={11} /> Carnê
|
||||||
|
|
@ -1315,18 +1330,33 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
|
|
||||||
let additions = 0;
|
let additions = 0;
|
||||||
const isOverdue = ['overdue', 'atrasado'].includes((payment.status || '').toLowerCase());
|
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) {
|
if (!isPaid && isOverdue && payment.dueDate) {
|
||||||
const dd = new Date(payment.dueDate + 'T00:00:00');
|
const dd = new Date(payment.dueDate + 'T00:00:00');
|
||||||
const tt = new Date(); tt.setHours(0,0,0,0);
|
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 dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
|
||||||
|
|
||||||
const student = data.students.find(s => s.id === payment.studentId);
|
const student = data.students.find(s => s.id === payment.studentId);
|
||||||
const studentClass = data.classes.find(c => c.id === student?.classId);
|
const studentClass = data.classes.find(c => c.id === student?.classId);
|
||||||
const course = data.courses.find(c => c.id === studentClass?.courseId);
|
const course = data.courses.find(c => c.id === studentClass?.courseId);
|
||||||
const pLateFee = Number(payment.lateFee) || Number(course?.finePercentage) || 2;
|
const pLateFee = Number(payment.lateFee ?? course?.finePercentage ?? 0);
|
||||||
const pInterest = Number(payment.interest) || Number(course?.interestPercentage) || 1;
|
const pInterest = Number(payment.interest ?? course?.interestPercentage ?? 0);
|
||||||
|
|
||||||
const fineAmt = baseA * (pLateFee / 100);
|
const fineAmt = baseA * (pLateFee / 100);
|
||||||
const intAmt = baseA * ((pInterest / 30) / 100) * dDays;
|
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">
|
<div className="font-black text-slate-900 text-sm">
|
||||||
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
R$ {(payment.amount + additions).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
</div>
|
</div>
|
||||||
{discount > 0 && (
|
{activeDiscount > 0 && (
|
||||||
<div className="text-[10px] text-emerald-600 font-bold">
|
<div className="text-[10px] text-emerald-600 font-bold">
|
||||||
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
- R$ {activeDiscount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{additions > 0 && (
|
{additions > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -318,8 +318,8 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar configurações de juros do curso do aluno
|
// Buscar configurações de juros do curso do aluno
|
||||||
let defaultLateFee = 2;
|
let defaultLateFee = 0;
|
||||||
let defaultInterest = 1;
|
let defaultInterest = 0;
|
||||||
try {
|
try {
|
||||||
const { rows: studentRows } = await pool.query('SELECT turma_id FROM alunos WHERE id = $1', [req.user.studentId]);
|
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) {
|
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) {
|
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]);
|
const { rows: cRows } = await pool.query('SELECT multa_percentual, juros_percentual FROM cursos WHERE id = $1', [tRows[0].curso_id]);
|
||||||
if (cRows.length > 0) {
|
if (cRows.length > 0) {
|
||||||
defaultLateFee = Number(cRows[0].multa_percentual) || 2;
|
defaultLateFee = cRows[0].multa_percentual !== null ? Number(cRows[0].multa_percentual) : 0;
|
||||||
defaultInterest = Number(cRows[0].juros_percentual) || 1;
|
defaultInterest = cRows[0].juros_percentual !== null ? Number(cRows[0].juros_percentual) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,22 +163,32 @@ export default function Financeiro() {
|
||||||
// Se está pago mas o banco ainda não sincronizou o valor_pago (fallback)
|
// Se está pago mas o banco ainda não sincronizou o valor_pago (fallback)
|
||||||
if (isPaid) return payment.amount - (payment.discount || 0);
|
if (isPaid) return payment.amount - (payment.discount || 0);
|
||||||
|
|
||||||
const baseAmount = payment.amount - (payment.discount || 0);
|
|
||||||
|
|
||||||
// Simulação Matemática de Atraso
|
// Simulação Matemática de Atraso
|
||||||
if (status === 'overdue' && payment.dueDate) {
|
if (status === 'overdue' && payment.dueDate) {
|
||||||
const dd = new Date(payment.dueDate + 'T00:00:00');
|
const dd = new Date(payment.dueDate + 'T00:00:00');
|
||||||
const tt = new Date(); tt.setHours(0,0,0,0);
|
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 dDays = Math.ceil((tt.getTime() - dd.getTime()) / 86400000);
|
||||||
const fineAmt = baseAmount * ((Number(payment.lateFee) || 2) / 100);
|
const fineAmt = baseAmount * ((payment.lateFee ?? 0) / 100);
|
||||||
const intAmt = baseAmount * (((Number(payment.interest) || 1) / 30) / 100) * dDays;
|
const intAmt = baseAmount * (((payment.interest ?? 0) / 30) / 100) * dDays;
|
||||||
return baseAmount + fineAmt + intAmt;
|
return baseAmount + fineAmt + intAmt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se está pendente ou atrasado sem juros aplicáveis, mostramos o líquido esperado
|
// 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
|
const totalPending = payments
|
||||||
|
|
@ -327,8 +337,8 @@ export default function Financeiro() {
|
||||||
<td style={{ fontWeight: 500 }}>
|
<td style={{ fontWeight: 500 }}>
|
||||||
{formatCurrency(payment.amount)}
|
{formatCurrency(payment.amount)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ color: payment.discount ? 'var(--color-success)' : 'var(--color-text-secondary)', fontSize: '0.8125rem' }}>
|
<td style={{ color: (payment.discount && normalizeStatus(payment) !== 'overdue') ? 'var(--color-success)' : 'var(--color-text-secondary)', fontSize: '0.8125rem' }}>
|
||||||
{payment.discount ? `- ${formatCurrency(payment.discount)}` : '—'}
|
{(payment.discount && normalizeStatus(payment) !== 'overdue') ? `- ${formatCurrency(payment.discount)}` : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td style={{
|
<td style={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue