feat(finance): migrate manually created payments to sql-first architecture with local_id support
This commit is contained in:
parent
a9f8559462
commit
27fcbada35
|
|
@ -1112,10 +1112,33 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}
|
{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4">
|
<td className="px-4 py-4">
|
||||||
<div className="font-bold text-slate-700 text-sm">R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
{(() => {
|
||||||
{!!payment.discount && payment.discount > 0 && (
|
const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((payment.status || '').toLowerCase());
|
||||||
<div className="text-[10px] text-emerald-600 font-bold">- R$ {payment.discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>
|
const valorPago = Number((payment as any).valor_pago || 0);
|
||||||
|
const discount = Number(payment.discount || 0);
|
||||||
|
|
||||||
|
if (isPaid) {
|
||||||
|
const displayValue = valorPago > 0 ? valorPago : (payment.amount - discount);
|
||||||
|
return (
|
||||||
|
<div className="font-bold text-slate-700 text-sm">
|
||||||
|
R$ {displayValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="font-bold text-slate-700 text-sm">
|
||||||
|
R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
{discount > 0 && (
|
||||||
|
<div className="text-[10px] text-emerald-600 font-bold">
|
||||||
|
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4">{getStatusBadge(payment)}</td>
|
<td className="px-4 py-4">{getStatusBadge(payment)}</td>
|
||||||
<td className="px-4 py-4">
|
<td className="px-4 py-4">
|
||||||
|
|
@ -1187,31 +1210,33 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-5 text-slate-600 text-sm">{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
<td className="px-4 py-5 text-slate-600 text-sm">{new Date(payment.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })}</td>
|
||||||
<td className="px-4 py-5">
|
<td className="px-4 py-5">
|
||||||
|
{(() => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (isPaid) {
|
||||||
|
const displayValue = valorPago > 0 ? valorPago : (payment.amount - discount);
|
||||||
|
return (
|
||||||
<div className="font-black text-slate-900 text-sm">
|
<div className="font-black text-slate-900 text-sm">
|
||||||
R$ {(() => {
|
R$ {displayValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
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';
|
|
||||||
const amtOrig = (payment as any).amount_original ? Number((payment as any).amount_original) : 0;
|
|
||||||
const valorPago = (payment as any).valor_pago ? Number((payment as any).valor_pago) : 0;
|
|
||||||
|
|
||||||
let bruto = amt;
|
|
||||||
if (amtOrig > bruto) bruto = amtOrig;
|
|
||||||
// Se está pago e o bruto atual parece ser o líquido, recompomos
|
|
||||||
if (isPaid && disc > 0 && bruto > 0 && (bruto === valorPago || (valorPago === 0 && bruto === amt))) {
|
|
||||||
bruto += disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bruto.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
{!!payment.discount && payment.discount > 0 && <div className="text-[10px] text-emerald-600 font-bold">- R$ {payment.discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</div>}
|
);
|
||||||
{(payment as any).valor_pago > 0 && (
|
} else {
|
||||||
<div className="text-[10px] text-blue-600 font-black mt-1 bg-blue-50 px-1 rounded inline-block">
|
return (
|
||||||
PAGO: R$ {Number((payment as any).valor_pago).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
<>
|
||||||
|
<div className="font-black text-slate-900 text-sm">
|
||||||
|
R$ {payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
{discount > 0 && (
|
||||||
|
<div className="text-[10px] text-emerald-600 font-bold">
|
||||||
|
- R$ {discount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-5">{getStatusBadge(payment)}</td>
|
<td className="px-4 py-5">{getStatusBadge(payment)}</td>
|
||||||
<td className="px-4 py-5">
|
<td className="px-4 py-5">
|
||||||
|
|
|
||||||
|
|
@ -906,7 +906,7 @@ app.delete('/api/admin/cobrancas', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ids } = req.body;
|
const { ids } = req.body;
|
||||||
if (!Array.isArray(ids)) return res.status(400).end();
|
if (!Array.isArray(ids)) return res.status(400).end();
|
||||||
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = ANY($1)', [ids]);
|
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = ANY($1) OR local_id = ANY($1)', [ids]);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
res.status(500).json({error: e.message});
|
res.status(500).json({error: e.message});
|
||||||
|
|
@ -915,7 +915,7 @@ app.delete('/api/admin/cobrancas', async (req, res) => {
|
||||||
|
|
||||||
app.delete('/api/admin/cobrancas/:id', async (req, res) => {
|
app.delete('/api/admin/cobrancas/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1', [req.params.id]);
|
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1 OR local_id = $1', [req.params.id]);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
res.status(500).json({error: e.message});
|
res.status(500).json({error: e.message});
|
||||||
|
|
@ -944,7 +944,7 @@ app.put('/api/admin/cobrancas/:id', async (req, res) => {
|
||||||
|
|
||||||
values.push(req.params.id);
|
values.push(req.params.id);
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE alunos_cobrancas SET ${updates.join(', ')} WHERE asaas_payment_id = $${paramIdx}`,
|
`UPDATE alunos_cobrancas SET ${updates.join(', ')} WHERE asaas_payment_id = $${paramIdx} OR local_id = $${paramIdx}`,
|
||||||
values
|
values
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1234,6 +1234,13 @@ app.post('/api/excluir_cobranca', async (req, res) => {
|
||||||
const { id } = req.body;
|
const { id } = req.body;
|
||||||
if (!id) return res.status(400).json({ error: 'ID não fornecido' });
|
if (!id) return res.status(400).json({ error: 'ID não fornecido' });
|
||||||
|
|
||||||
|
const isManual = id.startsWith('pay-');
|
||||||
|
|
||||||
|
if (isManual) {
|
||||||
|
await pool.query('DELETE FROM alunos_cobrancas WHERE local_id = $1', [id]);
|
||||||
|
return res.status(200).json({ message: 'Excluído na base local' });
|
||||||
|
}
|
||||||
|
|
||||||
const parcelas = await getCobrancasByOrQuery(id);
|
const parcelas = await getCobrancasByOrQuery(id);
|
||||||
let isSinglePayment = id.startsWith('pay_');
|
let isSinglePayment = id.startsWith('pay_');
|
||||||
|
|
||||||
|
|
@ -1243,11 +1250,13 @@ app.post('/api/excluir_cobranca', async (req, res) => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
addLog('Asaas', 'Exclusão Parcelamento OK', { id: asaasTargetId });
|
addLog('Asaas', 'Exclusão Parcelamento OK', { id: asaasTargetId });
|
||||||
}
|
}
|
||||||
|
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_installment_id = $1', [asaasTargetId]);
|
||||||
} else {
|
} else {
|
||||||
const resp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${id}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
|
const resp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${id}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
|
||||||
if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); }
|
if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); }
|
||||||
|
|
||||||
addLog('Asaas', 'Exclusão Cobrança OK', { id });
|
addLog('Asaas', 'Exclusão Cobrança OK', { id });
|
||||||
|
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1', [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({ message: 'Excluído no Asaas e na base local' });
|
return res.status(200).json({ message: 'Excluído no Asaas e na base local' });
|
||||||
|
|
@ -1309,17 +1318,24 @@ app.put('/api/cobrancas/:id', async (req, res) => {
|
||||||
if (parcelas.length > 0 && parcelas[0].asaas_payment_id) targetAsaasId = parcelas[0].asaas_payment_id;
|
if (parcelas.length > 0 && parcelas[0].asaas_payment_id) targetAsaasId = parcelas[0].asaas_payment_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAsaasPayment = targetAsaasId && targetAsaasId.startsWith('pay_');
|
||||||
|
|
||||||
|
if (isAsaasPayment) {
|
||||||
const aResp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${targetAsaasId}`, {
|
const aResp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${targetAsaasId}`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'access_token': process.env.ASAAS_API_KEY },
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'access_token': process.env.ASAAS_API_KEY },
|
||||||
body: JSON.stringify({ value: valor, dueDate: vencimento })
|
body: JSON.stringify({ value: valor, dueDate: vencimento })
|
||||||
});
|
});
|
||||||
if (!aResp.ok) { const err = await aResp.json().catch(() => ({})); return res.status(400).json({ error: err.errors?.[0]?.description || 'Erro Asaas' }); }
|
if (!aResp.ok) { const err = await aResp.json().catch(() => ({})); return res.status(400).json({ error: err.errors?.[0]?.description || 'Erro Asaas' }); }
|
||||||
|
}
|
||||||
|
|
||||||
const queryField = isUUID(id) ? 'id' : 'asaas_payment_id';
|
const queryField = isUUID(id) ? 'id' : (id.startsWith('pay_') ? 'asaas_payment_id' : 'local_id');
|
||||||
await updateCobrancaByField(queryField, id, { valor, vencimento });
|
await updateCobrancaByField(queryField, id, { valor, vencimento });
|
||||||
|
|
||||||
res.json({ message: 'Editado com sucesso' });
|
res.json({ message: 'Editado com sucesso' });
|
||||||
} catch (e) { res.status(500).json({ error: 'Erro interno.' }); }
|
} catch (e) {
|
||||||
|
console.error('[cobrancas:PUT] Erro:', e);
|
||||||
|
res.status(500).json({ error: 'Erro interno.' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/alunos/:id/carne', async (req, res) => {
|
app.get('/api/alunos/:id/carne', async (req, res) => {
|
||||||
|
|
@ -1797,7 +1813,11 @@ async function syncRelationalToJsonPayments() {
|
||||||
if (!appData || !appData.payments) return;
|
if (!appData || !appData.payments) return;
|
||||||
|
|
||||||
const updatedPayments = appData.payments.map(p => {
|
const updatedPayments = appData.payments.map(p => {
|
||||||
const match = cloudPayments.find(cp => cp.asaas_payment_id === p.asaasPaymentId);
|
const match = cloudPayments.find(cp => {
|
||||||
|
if (p.asaasPaymentId && cp.asaas_payment_id === p.asaasPaymentId) return true;
|
||||||
|
if (p.id && cp.local_id === p.id) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
if (match) {
|
if (match) {
|
||||||
const statusStr = (match.status || '').toLowerCase();
|
const statusStr = (match.status || '').toLowerCase();
|
||||||
const newStatus = statusStr === 'pago' ? 'paid' :
|
const newStatus = statusStr === 'pago' ? 'paid' :
|
||||||
|
|
|
||||||
|
|
@ -412,6 +412,8 @@ export async function syncJsonToRelationalTables() {
|
||||||
|
|
||||||
// 0. Garantir esquema atualizado
|
// 0. Garantir esquema atualizado
|
||||||
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS valor_pago NUMERIC(10,2) DEFAULT 0');
|
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS valor_pago NUMERIC(10,2) DEFAULT 0');
|
||||||
|
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS local_id VARCHAR(255)');
|
||||||
|
await client.query('CREATE INDEX IF NOT EXISTS idx_cobrancas_local_id ON alunos_cobrancas(local_id)');
|
||||||
|
|
||||||
// 1. Sincronizar Perfil da Escola (Configurações)
|
// 1. Sincronizar Perfil da Escola (Configurações)
|
||||||
if (data.periods && Array.isArray(data.periods)) {
|
if (data.periods && Array.isArray(data.periods)) {
|
||||||
|
|
@ -546,7 +548,7 @@ export async function syncJsonToRelationalTables() {
|
||||||
// 8. Sincronizar Cobranças (Financeiro) — com campos ricos para migração SQL-First
|
// 8. Sincronizar Cobranças (Financeiro) — com campos ricos para migração SQL-First
|
||||||
if (data.payments && Array.isArray(data.payments)) {
|
if (data.payments && Array.isArray(data.payments)) {
|
||||||
for (const p of data.payments) {
|
for (const p of data.payments) {
|
||||||
if (!p.asaasPaymentId || !p.studentId) continue;
|
if (!p.studentId || !p.id) continue;
|
||||||
|
|
||||||
// Normalizar status para o padrão SQL (maiúsculas)
|
// Normalizar status para o padrão SQL (maiúsculas)
|
||||||
const rawStatus = (p.status || 'pending').toLowerCase();
|
const rawStatus = (p.status || 'pending').toLowerCase();
|
||||||
|
|
@ -566,14 +568,17 @@ export async function syncJsonToRelationalTables() {
|
||||||
valorPago = Number(p.valor_pago || 0) || (amount - discount);
|
valorPago = Number(p.valor_pago || 0) || (amount - discount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p.asaasPaymentId) {
|
||||||
|
// Cobrança vinculada ao Asaas (usa ON CONFLICT em asaas_payment_id)
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO alunos_cobrancas (
|
`INSERT INTO alunos_cobrancas (
|
||||||
aluno_id, asaas_payment_id, asaas_installment_id, installment,
|
local_id, aluno_id, asaas_payment_id, asaas_installment_id, installment,
|
||||||
valor, vencimento, link_boleto, status,
|
valor, vencimento, link_boleto, status,
|
||||||
description, type, discount, installment_number, total_installments,
|
description, type, discount, installment_number, total_installments,
|
||||||
contract_id, asaas_payment_url, amount_original, data_pagamento, valor_pago
|
contract_id, asaas_payment_url, amount_original, data_pagamento, valor_pago
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||||
ON CONFLICT (asaas_payment_id) DO UPDATE SET
|
ON CONFLICT (asaas_payment_id) DO UPDATE SET
|
||||||
|
local_id = COALESCE(EXCLUDED.local_id, alunos_cobrancas.local_id),
|
||||||
aluno_id = EXCLUDED.aluno_id,
|
aluno_id = EXCLUDED.aluno_id,
|
||||||
asaas_installment_id = COALESCE(EXCLUDED.asaas_installment_id, alunos_cobrancas.asaas_installment_id),
|
asaas_installment_id = COALESCE(EXCLUDED.asaas_installment_id, alunos_cobrancas.asaas_installment_id),
|
||||||
installment = COALESCE(EXCLUDED.installment, alunos_cobrancas.installment),
|
installment = COALESCE(EXCLUDED.installment, alunos_cobrancas.installment),
|
||||||
|
|
@ -592,6 +597,7 @@ export async function syncJsonToRelationalTables() {
|
||||||
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento),
|
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento),
|
||||||
valor_pago = EXCLUDED.valor_pago`,
|
valor_pago = EXCLUDED.valor_pago`,
|
||||||
[
|
[
|
||||||
|
p.id,
|
||||||
p.studentId,
|
p.studentId,
|
||||||
p.asaasPaymentId,
|
p.asaasPaymentId,
|
||||||
p.asaasInstallmentId || p.installmentId || null,
|
p.asaasInstallmentId || p.installmentId || null,
|
||||||
|
|
@ -611,7 +617,71 @@ export async function syncJsonToRelationalTables() {
|
||||||
p.paidDate || null,
|
p.paidDate || null,
|
||||||
valorPago
|
valorPago
|
||||||
]
|
]
|
||||||
).catch(err => console.warn(`[Sync:Finance] Erro no boleto ${p.asaasPaymentId}:`, err.message));
|
).catch(err => console.warn(`[Sync:Finance] Erro no boleto Asaas ${p.asaasPaymentId}:`, err.message));
|
||||||
|
} else {
|
||||||
|
// Cobrança manual (sem asaasPaymentId)
|
||||||
|
// Verificamos por local_id para evitar duplicação
|
||||||
|
const existing = await client.query('SELECT id FROM alunos_cobrancas WHERE local_id = $1', [p.id]);
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE alunos_cobrancas SET
|
||||||
|
aluno_id = $1,
|
||||||
|
valor = $2,
|
||||||
|
vencimento = $3,
|
||||||
|
status = $4,
|
||||||
|
description = $5,
|
||||||
|
type = $6,
|
||||||
|
discount = $7,
|
||||||
|
installment_number = $8,
|
||||||
|
total_installments = $9,
|
||||||
|
contract_id = $10,
|
||||||
|
amount_original = $11,
|
||||||
|
data_pagamento = $12,
|
||||||
|
valor_pago = $13
|
||||||
|
WHERE local_id = $14`,
|
||||||
|
[
|
||||||
|
p.studentId,
|
||||||
|
valorBruto,
|
||||||
|
p.dueDate,
|
||||||
|
sqlStatus,
|
||||||
|
p.description || null,
|
||||||
|
p.type || 'monthly',
|
||||||
|
discount,
|
||||||
|
p.installmentNumber || null,
|
||||||
|
p.totalInstallments || null,
|
||||||
|
p.contractId || null,
|
||||||
|
valorBruto,
|
||||||
|
p.paidDate || null,
|
||||||
|
valorPago,
|
||||||
|
p.id
|
||||||
|
]
|
||||||
|
).catch(err => console.warn(`[Sync:Finance] Erro ao atualizar boleto manual ${p.id}:`, err.message));
|
||||||
|
} else {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO alunos_cobrancas (
|
||||||
|
local_id, aluno_id, valor, vencimento, status,
|
||||||
|
description, type, discount, installment_number, total_installments,
|
||||||
|
contract_id, amount_original, data_pagamento, valor_pago
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
|
||||||
|
[
|
||||||
|
p.id,
|
||||||
|
p.studentId,
|
||||||
|
valorBruto,
|
||||||
|
p.dueDate,
|
||||||
|
sqlStatus,
|
||||||
|
p.description || null,
|
||||||
|
p.type || 'monthly',
|
||||||
|
discount,
|
||||||
|
p.installmentNumber || null,
|
||||||
|
p.totalInstallments || null,
|
||||||
|
p.contractId || null,
|
||||||
|
valorBruto,
|
||||||
|
p.paidDate || null,
|
||||||
|
valorPago
|
||||||
|
]
|
||||||
|
).catch(err => console.warn(`[Sync:Finance] Erro ao inserir boleto manual ${p.id}:`, err.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,21 +248,26 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
|
|
||||||
// Criar mapa rápido do JSON por asaasPaymentId para lookup
|
// Criar mapa rápido do JSON por asaasPaymentId para lookup
|
||||||
const jsonMap = {};
|
const jsonMap = {};
|
||||||
|
const jsonLocalMap = {}; // Novo mapa por ID local do JSON
|
||||||
for (const jp of jsonPayments) {
|
for (const jp of jsonPayments) {
|
||||||
const key = jp.asaasPaymentId || jp.asaas_payment_id;
|
const key = jp.asaasPaymentId || jp.asaas_payment_id;
|
||||||
if (key) jsonMap[key] = jp;
|
if (key) jsonMap[key] = jp;
|
||||||
|
if (jp.id) jsonLocalMap[jp.id] = jp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com JSON quando SQL não tem o campo
|
// 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com JSON quando SQL não tem o campo
|
||||||
const seenAsaasIds = new Set();
|
const seenAsaasIds = new Set();
|
||||||
|
const seenLocalIds = new Set(); // Evitar duplicar itens adicionados por local_id
|
||||||
const finalPayments = [];
|
const finalPayments = [];
|
||||||
|
|
||||||
// 3a. Iterar sobre registros do SQL (fonte da verdade)
|
// 3a. Iterar sobre registros do SQL (fonte da verdade)
|
||||||
for (const db of dbRows) {
|
for (const db of dbRows) {
|
||||||
const asaasId = db.asaas_payment_id;
|
const asaasId = db.asaas_payment_id;
|
||||||
seenAsaasIds.add(asaasId);
|
const localId = db.local_id;
|
||||||
|
if (asaasId) seenAsaasIds.add(asaasId);
|
||||||
|
if (localId) seenLocalIds.add(localId);
|
||||||
|
|
||||||
const jsonP = jsonMap[asaasId] || {};
|
const jsonP = (asaasId ? jsonMap[asaasId] : null) || (localId ? jsonLocalMap[localId] : null) || {};
|
||||||
const dbStatus = (db.status || '').toLowerCase().trim();
|
const dbStatus = (db.status || '').toLowerCase().trim();
|
||||||
const normalizedStatus = statusMap[dbStatus] || 'pending';
|
const normalizedStatus = statusMap[dbStatus] || 'pending';
|
||||||
|
|
||||||
|
|
@ -302,9 +307,9 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
finalPayments.push({
|
finalPayments.push({
|
||||||
id: jsonP.id || asaasId,
|
id: localId || jsonP.id || asaasId,
|
||||||
studentId: req.user.studentId,
|
studentId: req.user.studentId,
|
||||||
asaasPaymentId: asaasId,
|
asaasPaymentId: asaasId || null,
|
||||||
asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null,
|
asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null,
|
||||||
amount: amountBruto,
|
amount: amountBruto,
|
||||||
discount: discount,
|
discount: discount,
|
||||||
|
|
@ -325,6 +330,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
||||||
for (const jp of jsonPayments) {
|
for (const jp of jsonPayments) {
|
||||||
const key = jp.asaasPaymentId || jp.asaas_payment_id;
|
const key = jp.asaasPaymentId || jp.asaas_payment_id;
|
||||||
if (key && seenAsaasIds.has(key)) continue; // já processado
|
if (key && seenAsaasIds.has(key)) continue; // já processado
|
||||||
|
if (jp.id && seenLocalIds.has(jp.id)) continue; // já processado via local_id
|
||||||
if (!key && !jp.id) continue; // registro inválido
|
if (!key && !jp.id) continue; // registro inválido
|
||||||
|
|
||||||
const jpStatus = (jp.status || '').toLowerCase().trim();
|
const jpStatus = (jp.status || '').toLowerCase().trim();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue