Phase 1: Add rich columns to alunos_cobrancas and migrate JSON metadata to SQL on boot
This commit is contained in:
parent
00351031d1
commit
b440023add
|
|
@ -1620,6 +1620,32 @@ async function inicializarAgendamento() {
|
|||
ALTER TABLE alunos_cobrancas ADD COLUMN last_overdue_warning_at TIMESTAMP WITH TIME ZONE;
|
||||
END IF;
|
||||
|
||||
-- ===== FASE 1: Colunas ricas para migração financeira SQL-First =====
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='description') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN description TEXT;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='type') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN type TEXT DEFAULT 'monthly';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='discount') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN discount NUMERIC(10,2) DEFAULT 0;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='installment_number') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN installment_number INTEGER;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='total_installments') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN total_installments INTEGER;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='contract_id') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN contract_id TEXT;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='asaas_payment_url') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN asaas_payment_url TEXT;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='amount_original') THEN
|
||||
ALTER TABLE alunos_cobrancas ADD COLUMN amount_original NUMERIC(10,2);
|
||||
END IF;
|
||||
|
||||
-- Garantir índice de unicidade para o UPSERT funcionar
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'alunos_cobrancas' AND indexname = 'idx_asaas_payment_id_unique') THEN
|
||||
CREATE UNIQUE INDEX idx_asaas_payment_id_unique ON alunos_cobrancas(asaas_payment_id);
|
||||
|
|
|
|||
|
|
@ -540,24 +540,40 @@ export async function syncJsonToRelationalTables() {
|
|||
}
|
||||
}
|
||||
|
||||
// 8. Sincronizar Cobranças (Financeiro)
|
||||
// 8. Sincronizar Cobranças (Financeiro) — com campos ricos para migração SQL-First
|
||||
if (data.payments && Array.isArray(data.payments)) {
|
||||
for (const p of data.payments) {
|
||||
if (!p.asaasPaymentId || !p.studentId) continue;
|
||||
|
||||
// Normalizar status para o padrão SQL (maiúsculas)
|
||||
const rawStatus = (p.status || 'pending').toLowerCase();
|
||||
const statusMap = { 'paid': 'PAGO', 'received': 'PAGO', 'confirmed': 'PAGO', 'overdue': 'ATRASADO', 'cancelled': 'CANCELADO' };
|
||||
const sqlStatus = statusMap[rawStatus] || 'PENDENTE';
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO alunos_cobrancas (
|
||||
aluno_id, asaas_payment_id, asaas_installment_id, installment,
|
||||
valor, vencimento, link_boleto, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
valor, vencimento, link_boleto, status,
|
||||
description, type, discount, installment_number, total_installments,
|
||||
contract_id, asaas_payment_url, amount_original, data_pagamento
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
ON CONFLICT (asaas_payment_id) DO UPDATE SET
|
||||
aluno_id = EXCLUDED.aluno_id,
|
||||
asaas_installment_id = EXCLUDED.asaas_installment_id,
|
||||
installment = EXCLUDED.installment,
|
||||
asaas_installment_id = COALESCE(EXCLUDED.asaas_installment_id, alunos_cobrancas.asaas_installment_id),
|
||||
installment = COALESCE(EXCLUDED.installment, alunos_cobrancas.installment),
|
||||
valor = EXCLUDED.valor,
|
||||
vencimento = EXCLUDED.vencimento,
|
||||
link_boleto = EXCLUDED.link_boleto,
|
||||
status = EXCLUDED.status`,
|
||||
link_boleto = COALESCE(EXCLUDED.link_boleto, alunos_cobrancas.link_boleto),
|
||||
status = CASE WHEN alunos_cobrancas.status = 'PAGO' THEN alunos_cobrancas.status ELSE EXCLUDED.status END,
|
||||
description = COALESCE(EXCLUDED.description, alunos_cobrancas.description),
|
||||
type = COALESCE(EXCLUDED.type, alunos_cobrancas.type),
|
||||
discount = COALESCE(EXCLUDED.discount, alunos_cobrancas.discount),
|
||||
installment_number = COALESCE(EXCLUDED.installment_number, alunos_cobrancas.installment_number),
|
||||
total_installments = COALESCE(EXCLUDED.total_installments, alunos_cobrancas.total_installments),
|
||||
contract_id = COALESCE(EXCLUDED.contract_id, alunos_cobrancas.contract_id),
|
||||
asaas_payment_url = COALESCE(EXCLUDED.asaas_payment_url, alunos_cobrancas.asaas_payment_url),
|
||||
amount_original = COALESCE(EXCLUDED.amount_original, alunos_cobrancas.amount_original),
|
||||
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento)`,
|
||||
[
|
||||
p.studentId,
|
||||
p.asaasPaymentId,
|
||||
|
|
@ -566,7 +582,16 @@ export async function syncJsonToRelationalTables() {
|
|||
p.amount || 0,
|
||||
p.dueDate,
|
||||
p.bankSlipUrl || p.link || null,
|
||||
(p.status || 'PENDENTE').toUpperCase()
|
||||
sqlStatus,
|
||||
p.description || null,
|
||||
p.type || 'monthly',
|
||||
p.discount || 0,
|
||||
p.installmentNumber || null,
|
||||
p.totalInstallments || null,
|
||||
p.contractId || null,
|
||||
p.asaasPaymentUrl || null,
|
||||
p.amount || null,
|
||||
p.paidDate || null
|
||||
]
|
||||
).catch(err => console.warn(`[Sync:Finance] Erro no boleto ${p.asaasPaymentId}:`, err.message));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,9 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
|||
`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
|
||||
link_boleto, link_carne, transaction_receipt_url,
|
||||
description, type, discount, installment_number, total_installments,
|
||||
contract_id, asaas_payment_url, amount_original
|
||||
FROM alunos_cobrancas
|
||||
WHERE aluno_id = $1
|
||||
ORDER BY vencimento ASC`,
|
||||
|
|
@ -243,7 +245,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
|||
console.error('Financeiro: erro ao buscar do PostgreSQL -', dbErr.message);
|
||||
}
|
||||
|
||||
// 2. FONTE SECUNDÁRIA: JSON (school_data.payments) — metadados complementares
|
||||
// 2. FONTE SECUNDÁRIA: JSON (school_data.payments) — fallback para dados que ainda não migraram
|
||||
const schoolData = await getSchoolData();
|
||||
const jsonPayments = (schoolData.payments || []).filter((p) => p.studentId === req.user.studentId);
|
||||
|
||||
|
|
@ -254,7 +256,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
|||
if (key) jsonMap[key] = jp;
|
||||
}
|
||||
|
||||
// 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com metadados do JSON
|
||||
// 3. CONSTRUIR LISTA FINAL: SQL como base, enriquecido com JSON quando SQL não tem o campo
|
||||
const seenAsaasIds = new Set();
|
||||
const finalPayments = [];
|
||||
|
||||
|
|
@ -267,11 +269,10 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => {
|
|||
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;
|
||||
// Parcela: SQL tem prioridade, depois JSON, depois inferência por grupo
|
||||
let installmentNumber = db.installment_number || jsonP.installmentNumber || null;
|
||||
let totalInstallments = db.total_installments || 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) {
|
||||
|
|
@ -280,20 +281,22 @@ 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;
|
||||
const discount = Number(db.discount) || (jsonP.amount ? (jsonP.discount || 0) : 0);
|
||||
|
||||
finalPayments.push({
|
||||
id: jsonP.id || asaasId,
|
||||
studentId: req.user.studentId,
|
||||
asaasPaymentId: asaasId,
|
||||
asaasPaymentUrl: jsonP.asaasPaymentUrl || null,
|
||||
// jsonP.amount = valor BRUTO (ex: 170), db.valor = valor LÍQUIDO do Asaas (ex: 150)
|
||||
// Se o JSON tem o bruto, usa ele + desconto separado. Se não, usa SQL (já líquido) sem desconto.
|
||||
amount: jsonP.amount || Number(db.valor) || 0,
|
||||
discount: jsonP.amount ? (jsonP.discount || 0) : 0,
|
||||
asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null,
|
||||
amount: amountOriginal,
|
||||
discount: discount,
|
||||
dueDate: db.vencimento || jsonP.dueDate,
|
||||
status: normalizedStatus,
|
||||
paidDate: db.data_pagamento || jsonP.paidDate || null,
|
||||
type: jsonP.type || 'monthly',
|
||||
description: jsonP.description || null,
|
||||
type: db.type || jsonP.type || 'monthly',
|
||||
description: db.description || jsonP.description || null,
|
||||
installmentNumber,
|
||||
totalInstallments,
|
||||
link_boleto: db.link_boleto || jsonP.bankSlipUrl || null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue