From 58182ff53c1379230687f4033aa6b6cf4ed75eef Mon Sep 17 00:00:00 2001 From: Sidney Date: Thu, 14 May 2026 22:03:18 -0300 Subject: [PATCH] Fix: preserve gross amount in webhook and reconstruct it in portal to fix double discount on paid items --- manager/scratch/portal_debug.js | 89 +++++++++++++++++++++++++++++++++ manager/server.selfhosted.js | 8 ++- portal/server.js | 12 ++++- portal/server.selfhosted.js | 9 +++- 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 manager/scratch/portal_debug.js diff --git a/manager/scratch/portal_debug.js b/manager/scratch/portal_debug.js new file mode 100644 index 0000000..4477128 --- /dev/null +++ b/manager/scratch/portal_debug.js @@ -0,0 +1,89 @@ +import pg from 'pg'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const pool = new pg.Pool({ + connectionString: process.env.DATABASE_URL || 'postgresql://edumanager:edumanager2024@localhost:5432/edumanager' +}); + +async function debug() { + try { + // 1. Buscar todos os alunos + const alunos = await pool.query('SELECT id, nome FROM alunos LIMIT 20'); + console.log('\n=== ALUNOS CADASTRADOS ==='); + alunos.rows.forEach(a => console.log(` ${a.id} | ${a.nome}`)); + + // 2. Buscar TODAS as cobranças do SQL + const cobrancas = await pool.query(` + SELECT asaas_payment_id, aluno_id, status, + TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento, + valor, amount_original, discount, description, type, + TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento, + transaction_receipt_url + FROM alunos_cobrancas + ORDER BY aluno_id, vencimento ASC + `); + + console.log(`\n=== COBRANÇAS NO SQL (${cobrancas.rows.length} total) ===`); + const byStudent = {}; + cobrancas.rows.forEach(c => { + if (!byStudent[c.aluno_id]) byStudent[c.aluno_id] = []; + byStudent[c.aluno_id].push(c); + }); + + for (const [alunoId, payments] of Object.entries(byStudent)) { + const aluno = alunos.rows.find(a => a.id === alunoId); + console.log(`\n 📌 ${aluno?.nome || alunoId} (${payments.length} cobranças):`); + payments.forEach(p => { + const status = p.status?.toUpperCase(); + const icon = status === 'PAGO' ? '✅' : status === 'PENDENTE' ? '⏳' : status === 'ATRASADO' ? '🔴' : '❓'; + console.log(` ${icon} ${p.asaas_payment_id} | ${p.vencimento} | R$${Number(p.valor).toFixed(2)} | Status: ${p.status} | Pago em: ${p.data_pagamento || '-'} | Desc: ${p.description || '-'} | amount_original: ${p.amount_original || '-'}`); + }); + } + + // 3. Buscar do JSON + const dataPath = path.join(__dirname, '..', 'data', 'school_data.json'); + if (fs.existsSync(dataPath)) { + const data = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); + const jsonPayments = data.payments || []; + console.log(`\n=== COBRANÇAS NO JSON (${jsonPayments.length} total) ===`); + + const jsonByStudent = {}; + jsonPayments.forEach(p => { + if (!jsonByStudent[p.studentId]) jsonByStudent[p.studentId] = []; + jsonByStudent[p.studentId].push(p); + }); + + for (const [studentId, payments] of Object.entries(jsonByStudent)) { + const aluno = alunos.rows.find(a => a.id === studentId); + console.log(`\n 📌 ${aluno?.nome || studentId} (${payments.length} cobranças no JSON):`); + payments.forEach(p => { + const inSql = cobrancas.rows.find(c => c.asaas_payment_id === p.asaasPaymentId); + console.log(` ${inSql ? '🔗' : '⚠️'} ${p.asaasPaymentId || 'SEM_ID'} | ${p.dueDate} | R$${p.amount} | Status: ${p.status} | Desc: ${p.description || '-'} | discount: ${p.discount || 0} | ${inSql ? 'Existe no SQL' : 'SÓ NO JSON!'}`); + }); + } + + // 4. Verificar cobranças que existem no SQL mas NÃO no JSON + console.log('\n=== COBRANÇAS QUE EXISTEM NO SQL MAS NÃO NO JSON ==='); + let orphanCount = 0; + cobrancas.rows.forEach(c => { + const inJson = jsonPayments.find(p => p.asaasPaymentId === c.asaas_payment_id); + if (!inJson) { + orphanCount++; + console.log(` ⚠️ ${c.asaas_payment_id} | Aluno: ${c.aluno_id} | ${c.vencimento} | R$${Number(c.valor).toFixed(2)} | Status: ${c.status}`); + } + }); + if (orphanCount === 0) console.log(' ✅ Nenhuma — tudo sincronizado.'); + } + + } catch (err) { + console.error('Erro:', err); + } finally { + await pool.end(); + } +} + +debug(); diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index c4e2223..22155d0 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -834,16 +834,20 @@ app.post('/api/webhook_asaas', async (req, res) => { statusStr === 'atrasado' ? 'overdue' : statusStr === 'cancelado' ? 'cancelled' : 'pending'; + // Se for um evento de atualização de pagamento, atualiza o valor. + // Se for só confirmação de recebimento, preserva o 'amount' bruto original para não causar double-discount. + const shouldUpdateAmount = payload.event === 'PAYMENT_UPDATED' && updateData.valor; + appData.payments[pIdx] = { ...p, status: newStatus, - amount: updateData.valor || p.amount, + amount: shouldUpdateAmount ? updateData.valor : p.amount, dueDate: updateData.vencimento || p.dueDate, paidDate: updateData.data_pagamento || p.paidDate }; appData.lastUpdated = new Date().toISOString(); await saveSchoolData(appData); - console.log(`[Webhook:Sync] JSON atualizado para boleto ${asaasPaymentId}`); + console.log(`[Webhook:Sync] JSON atualizado para boleto ${asaasPaymentId} (Amount: ${shouldUpdateAmount ? 'Atualizado' : 'Preservado'})`); } } catch (syncErr) { console.error('[Webhook:Sync] Erro ao sincronizar JSON:', syncErr.message); diff --git a/portal/server.js b/portal/server.js index 1b0b7f6..1cffeb6 100644 --- a/portal/server.js +++ b/portal/server.js @@ -275,13 +275,21 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { } } + let amountOriginal = jsonP.amount || Number(db.valor) || 0; + const discount = jsonP.amount ? (jsonP.discount || 0) : 0; + + // [Bugfix]: Recupera o valor bruto corrompido pelo webhook antigo + if (amountOriginal === Number(db.valor) && discount > 0) { + amountOriginal += discount; + } + finalPayments.push({ id: jsonP.id || asaasId, studentId: req.user.studentId, asaasPaymentId: asaasId, asaasPaymentUrl: jsonP.asaasPaymentUrl || null, - amount: jsonP.amount || Number(db.valor) || 0, - discount: jsonP.amount ? (jsonP.discount || 0) : 0, + amount: amountOriginal, + discount: discount, dueDate: db.vencimento || jsonP.dueDate, status: normalizedStatus, paidDate: db.data_pagamento || jsonP.paidDate || null, diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index 2987d58..1cbf63a 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -279,9 +279,16 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { } // amount_original = valor bruto (ex: 170), db.valor = valor líquido Asaas (ex: 150) - const amountOriginal = Number(db.amount_original) || jsonP.amount || Number(db.valor) || 0; + let amountOriginal = Number(db.amount_original) || jsonP.amount || Number(db.valor) || 0; const discount = Number(db.discount) || (jsonP.amount ? (jsonP.discount || 0) : 0); + // [Bugfix]: Se o amountOriginal for igual ao valor líquido (db.valor) e houver desconto, + // significa que o webhook antigo sobrescreveu o valor bruto pelo líquido no JSON. + // Neste caso, o valor bruto real é o líquido + desconto. + if (amountOriginal === Number(db.valor) && discount > 0) { + amountOriginal += discount; + } + finalPayments.push({ id: jsonP.id || asaasId, studentId: req.user.studentId,