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;
|
||||
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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue