Fix: use PostgreSQL as primary source for Portal financial data (SQL-First) to show paid installments

This commit is contained in:
Sidney 2026-05-14 16:39:12 -03:00
parent 66139bff0d
commit c2efa1729f
2 changed files with 186 additions and 39 deletions

View File

@ -215,13 +215,105 @@ app.get('/api/portal/me', authMiddleware, async (req, res) => {
} }
}); });
// GET /api/portal/financeiro // GET /api/portal/financeiro (PostgreSQL como fonte primária — SQL-First)
app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
try { try {
const statusMap = {
'pago': 'paid', 'paid': 'paid', 'received': 'paid', 'confirmed': 'paid', 'received_in_cash': 'paid',
'atrasado': 'overdue', 'overdue': 'overdue', 'vencido': 'overdue',
'pendente': 'pending', 'pending': 'pending',
'cancelado': 'cancelled', 'cancelled': 'cancelled', 'refunded': 'cancelled', 'deleted': 'cancelled'
};
// 1. FONTE PRIMÁRIA: PostgreSQL (alunos_cobrancas)
let dbRows = [];
try {
const { rows } = await pool.query(
`SELECT asaas_payment_id, asaas_installment_id, installment,
valor, TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento,
status, TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento,
link_boleto, link_carne, transaction_receipt_url
FROM alunos_cobrancas
WHERE aluno_id = $1
ORDER BY vencimento ASC`,
[req.user.studentId]
);
dbRows = rows || [];
} catch (dbErr) {
console.error('Financeiro: erro ao buscar do PostgreSQL -', dbErr.message);
}
// 2. FONTE SECUNDÁRIA: JSON (school_data.payments)
const schoolData = await getSchoolData(); const schoolData = await getSchoolData();
const payments = (schoolData.payments || []).filter((p) => p.studentId === req.user.studentId); const jsonPayments = (schoolData.payments || []).filter((p) => p.studentId === req.user.studentId);
res.json({ payments });
const jsonMap = {};
for (const jp of jsonPayments) {
const key = jp.asaasPaymentId || jp.asaas_payment_id;
if (key) jsonMap[key] = jp;
}
// 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com metadados do JSON
const seenAsaasIds = new Set();
const finalPayments = [];
for (const db of dbRows) {
const asaasId = db.asaas_payment_id;
seenAsaasIds.add(asaasId);
const jsonP = jsonMap[asaasId] || {};
const dbStatus = (db.status || '').toLowerCase().trim();
const normalizedStatus = statusMap[dbStatus] || 'pending';
let installmentNumber = jsonP.installmentNumber || null;
let totalInstallments = jsonP.totalInstallments || null;
if (!installmentNumber && db.asaas_installment_id) {
const siblings = dbRows.filter(r => r.asaas_installment_id === db.asaas_installment_id);
if (siblings.length > 1) {
totalInstallments = siblings.length;
installmentNumber = siblings.indexOf(db) + 1;
}
}
finalPayments.push({
id: jsonP.id || asaasId,
studentId: req.user.studentId,
asaasPaymentId: asaasId,
asaasPaymentUrl: jsonP.asaasPaymentUrl || null,
amount: Number(db.valor) || jsonP.amount || 0,
discount: jsonP.discount || 0,
dueDate: db.vencimento || jsonP.dueDate,
status: normalizedStatus,
paidDate: db.data_pagamento || jsonP.paidDate || null,
type: jsonP.type || 'monthly',
description: jsonP.description || null,
installmentNumber,
totalInstallments,
link_boleto: db.link_boleto || jsonP.bankSlipUrl || null,
transactionReceiptUrl: db.transaction_receipt_url || jsonP.transactionReceiptUrl || null,
});
}
// Adicionar pagamentos que existem APENAS no JSON
for (const jp of jsonPayments) {
const key = jp.asaasPaymentId || jp.asaas_payment_id;
if (key && seenAsaasIds.has(key)) continue;
if (!key && !jp.id) continue;
const jpStatus = (jp.status || '').toLowerCase().trim();
const normalizedStatus = statusMap[jpStatus] || 'pending';
finalPayments.push({
...jp,
status: normalizedStatus,
amount: Number(jp.amount) || 0,
});
}
res.json({ payments: finalPayments });
} catch (err) { } catch (err) {
console.error('Financeiro error:', err);
res.status(500).json({ error: 'Erro interno' }); res.status(500).json({ error: 'Erro interno' });
} }
}); });
@ -230,7 +322,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
app.get('/api/portal/boletos', authMiddleware, async (req, res) => { app.get('/api/portal/boletos', authMiddleware, async (req, res) => {
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
'SELECT * FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC', `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] [req.user.studentId]
); );
res.json({ boletos: rows || [] }); res.json({ boletos: rows || [] });

View File

@ -215,52 +215,107 @@ app.get('/api/portal/me', authMiddleware, async (req, res) => {
} }
}); });
// GET /api/portal/financeiro (Mesclagem JSON + PostgreSQL para status atualizado) // GET /api/portal/financeiro (PostgreSQL como fonte primária — SQL-First)
app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
try { try {
const schoolData = await getSchoolData();
const payments = (schoolData.payments || []).filter((p) => p.studentId === req.user.studentId);
// Buscar status atualizado do PostgreSQL (fonte da verdade para pagamentos)
let dbBoletos = [];
try {
const { rows } = await pool.query(
'SELECT asaas_payment_id, status, valor, data_pagamento, transaction_receipt_url FROM alunos_cobrancas WHERE aluno_id = $1',
[req.user.studentId]
);
dbBoletos = rows || [];
} catch (dbErr) {
console.error('Financeiro: fallback to JSON only -', dbErr.message);
}
// Mesclar: se o PostgreSQL tem um status mais atualizado, ele prevalece
const statusMap = { const statusMap = {
'pago': 'paid', 'paid': 'paid', 'received': 'paid', 'confirmed': 'paid', 'pago': 'paid', 'paid': 'paid', 'received': 'paid', 'confirmed': 'paid', 'received_in_cash': 'paid',
'atrasado': 'overdue', 'overdue': 'overdue', 'vencido': 'overdue', 'atrasado': 'overdue', 'overdue': 'overdue', 'vencido': 'overdue',
'pendente': 'pending', 'pending': 'pending', 'pendente': 'pending', 'pending': 'pending',
'cancelado': 'cancelled', 'cancelled': 'cancelled' 'cancelado': 'cancelled', 'cancelled': 'cancelled', 'refunded': 'cancelled', 'deleted': 'cancelled'
}; };
const enrichedPayments = payments.map(p => { // 1. FONTE PRIMÁRIA: PostgreSQL (alunos_cobrancas) — contém TODOS os pagamentos criados
const asaasId = p.asaasPaymentId || p.asaas_payment_id; let dbRows = [];
if (!asaasId) return p; try {
const { rows } = await pool.query(
`SELECT asaas_payment_id, asaas_installment_id, installment,
valor, TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento,
status, TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento,
link_boleto, link_carne, transaction_receipt_url
FROM alunos_cobrancas
WHERE aluno_id = $1
ORDER BY vencimento ASC`,
[req.user.studentId]
);
dbRows = rows || [];
} catch (dbErr) {
console.error('Financeiro: erro ao buscar do PostgreSQL -', dbErr.message);
}
const dbRecord = dbBoletos.find(b => b.asaas_payment_id === asaasId); // 2. FONTE SECUNDÁRIA: JSON (school_data.payments) — metadados complementares
if (!dbRecord) return p; const schoolData = await getSchoolData();
const jsonPayments = (schoolData.payments || []).filter((p) => p.studentId === req.user.studentId);
const dbStatus = (dbRecord.status || '').toLowerCase().trim(); // Criar mapa rápido do JSON por asaasPaymentId para lookup
const normalizedStatus = statusMap[dbStatus] || p.status; const jsonMap = {};
for (const jp of jsonPayments) {
const key = jp.asaasPaymentId || jp.asaas_payment_id;
if (key) jsonMap[key] = jp;
}
return { // 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com metadados do JSON
...p, const seenAsaasIds = new Set();
const finalPayments = [];
// 3a. Iterar sobre registros do SQL (fonte da verdade)
for (const db of dbRows) {
const asaasId = db.asaas_payment_id;
seenAsaasIds.add(asaasId);
const jsonP = jsonMap[asaasId] || {};
const dbStatus = (db.status || '').toLowerCase().trim();
const normalizedStatus = statusMap[dbStatus] || 'pending';
// Calcular número da parcela se disponível
let installmentNumber = jsonP.installmentNumber || null;
let totalInstallments = jsonP.totalInstallments || null;
// Se não temos info de parcela no JSON, tentar inferir do installment group
if (!installmentNumber && db.asaas_installment_id) {
const siblings = dbRows.filter(r => r.asaas_installment_id === db.asaas_installment_id);
if (siblings.length > 1) {
totalInstallments = siblings.length;
installmentNumber = siblings.indexOf(db) + 1;
}
}
finalPayments.push({
id: jsonP.id || asaasId,
studentId: req.user.studentId,
asaasPaymentId: asaasId,
asaasPaymentUrl: jsonP.asaasPaymentUrl || null,
amount: Number(db.valor) || jsonP.amount || 0,
discount: jsonP.discount || 0,
dueDate: db.vencimento || jsonP.dueDate,
status: normalizedStatus, status: normalizedStatus,
amount: Number(dbRecord.valor) || p.amount, paidDate: db.data_pagamento || jsonP.paidDate || null,
paidDate: dbRecord.data_pagamento || p.paidDate, type: jsonP.type || 'monthly',
transactionReceiptUrl: dbRecord.transaction_receipt_url || p.transactionReceiptUrl description: jsonP.description || null,
}; installmentNumber,
totalInstallments,
link_boleto: db.link_boleto || jsonP.bankSlipUrl || null,
transactionReceiptUrl: db.transaction_receipt_url || jsonP.transactionReceiptUrl || null,
}); });
}
res.json({ payments: enrichedPayments }); // 3b. Adicionar pagamentos que existem APENAS no JSON (cobranças manuais/legadas sem asaas)
for (const jp of jsonPayments) {
const key = jp.asaasPaymentId || jp.asaas_payment_id;
if (key && seenAsaasIds.has(key)) continue; // já processado
if (!key && !jp.id) continue; // registro inválido
const jpStatus = (jp.status || '').toLowerCase().trim();
const normalizedStatus = statusMap[jpStatus] || 'pending';
finalPayments.push({
...jp,
status: normalizedStatus,
amount: Number(jp.amount) || 0,
});
}
res.json({ payments: finalPayments });
} catch (err) { } catch (err) {
console.error('Financeiro error:', err); console.error('Financeiro error:', err);
res.status(500).json({ error: 'Erro interno' }); res.status(500).json({ error: 'Erro interno' });
@ -271,7 +326,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
app.get('/api/portal/boletos', authMiddleware, async (req, res) => { app.get('/api/portal/boletos', authMiddleware, async (req, res) => {
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
'SELECT * FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC', `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] [req.user.studentId]
); );
res.json({ boletos: rows || [] }); res.json({ boletos: rows || [] });