edumanagerpro2/manager/services/database.js

633 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ============================================================
* SERVIÇO DE BANCO DE DADOS — PostgreSQL (Self-Hosted)
* Substitui todas as chamadas supabase.from(...) do sistema
* ============================================================
*/
import pg from 'pg';
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager';
const pool = new pg.Pool({
connectionString: DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
pool.on('error', (err) => {
console.error('[PostgreSQL] Erro inesperado no pool:', err);
});
// ============================================================
// HELPER: Buscar school_data JSON blob (compatibilidade legada)
// ============================================================
export async function getSchoolData() {
const { rows } = await pool.query(
'SELECT data FROM school_data WHERE id = 1'
);
return rows[0]?.data || {};
}
/**
* Percorre as aulas concluídas e gera registros de falta para alunos que não compareceram.
* Isso transforma faltas "virtuais" em registros físicos no banco de dados.
*/
export async function processAutoAbsences(data) {
if (!data.lessons || !data.students || !data.attendance) return data;
const now = new Date();
let updated = false;
// Cache de alunos por turma para performance
const studentsByClass = {};
data.lessons.forEach(lesson => {
const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00`;
const lessonEnd = new Date(lessonEndStr);
if (now > lessonEnd && lesson.status !== 'cancelled') {
if (!studentsByClass[lesson.classId]) {
studentsByClass[lesson.classId] = data.students.filter(s => s.classId === lesson.classId && s.status === 'active');
}
studentsByClass[lesson.classId].forEach(student => {
const hasRecord = data.attendance.some(a =>
a.studentId === student.id &&
(a.lessonId === lesson.id || (a.date && a.date.startsWith(lesson.date)))
);
if (!hasRecord) {
data.attendance.push({
id: `auto-abs-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
studentId: student.id,
classId: lesson.classId,
lessonId: lesson.id,
date: `${lesson.date}T${lesson.startTime || '00:00'}:00`,
type: 'absence',
verified: true,
autoGenerated: true
});
updated = true;
}
});
}
});
if (updated) {
data.lastUpdated = new Date().toISOString();
}
return data;
}
export async function saveSchoolData(data) {
// Aplicar fechamento de pauta automático antes de salvar
const dataWithAbsences = await processAutoAbsences(data);
await pool.query(
`INSERT INTO school_data (id, data, updated_at)
VALUES (1, $1, NOW())
ON CONFLICT (id) DO UPDATE SET data = $1, updated_at = NOW()`,
[JSON.stringify(dataWithAbsences)]
);
// Sincronizar tabelas relacionais em background para não travar o salvamento
syncJsonToRelationalTables(dataWithAbsences).catch(err =>
console.error('[Database:Sync] Erro na sincronização automática:', err)
);
}
// ============================================================
// HELPERS: alunos_cobrancas
// ============================================================
export async function insertCobrancas(cobrancas) {
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const c of cobrancas) {
await client.query(
`INSERT INTO alunos_cobrancas
(aluno_id, asaas_customer_id, asaas_payment_id, asaas_installment_id, installment, valor, vencimento, link_boleto, amount_original)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[c.aluno_id, c.asaas_customer_id, c.asaas_payment_id, c.asaas_installment_id || c.installment, c.installment, c.valor, c.vencimento, c.link_boleto, c.valor]
);
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
export async function updateCobranca(asaasPaymentId, updateData) {
const setClauses = [];
const values = [];
let i = 1;
for (const [key, value] of Object.entries(updateData)) {
if (value !== undefined) {
setClauses.push(`${key} = $${i}`);
values.push(value);
i++;
}
}
if (setClauses.length === 0) return;
values.push(asaasPaymentId);
await pool.query(
`UPDATE alunos_cobrancas SET ${setClauses.join(', ')} WHERE asaas_payment_id = $${i}`,
values
);
}
export async function deleteCobranca(asaasPaymentId) {
await pool.query(
'DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1',
[asaasPaymentId]
);
}
export async function getCobrancaByPaymentId(asaasPaymentId) {
const { rows } = await pool.query(
'SELECT * FROM alunos_cobrancas WHERE asaas_payment_id = $1',
[asaasPaymentId]
);
return rows[0] || null;
}
export async function getCobrancasByOrQuery(id) {
// Replicates: supabase.from('alunos_cobrancas').select('*').or(...)
const { rows } = await pool.query(
`SELECT * FROM alunos_cobrancas
WHERE installment = $1
OR asaas_installment_id = $1
OR asaas_payment_id = $1
OR id::text = $1
ORDER BY vencimento ASC`,
[id]
);
return rows;
}
export async function getCobrancasByAlunoId(alunoId) {
const { rows } = await pool.query(
'SELECT * FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC',
[alunoId]
);
return rows;
}
export async function getCobrancasPendentes() {
const { rows } = await pool.query(
"SELECT * FROM alunos_cobrancas WHERE status = 'PENDENTE'"
);
return rows;
}
export async function getCobrancasAtrasadas() {
const { rows } = await pool.query(
"SELECT * FROM alunos_cobrancas WHERE status = 'ATRASADO'"
);
return rows;
}
export async function getCobrancasByInstallmentId(installmentId) {
const { rows } = await pool.query(
'SELECT * FROM alunos_cobrancas WHERE asaas_installment_id = $1 ORDER BY vencimento ASC',
[installmentId]
);
return rows;
}
export async function updateCobrancaLinkCarne(installmentId, linkCarne) {
await pool.query(
'UPDATE alunos_cobrancas SET link_carne = $1 WHERE asaas_installment_id = $2',
[linkCarne, installmentId]
);
}
export async function updateCobrancaByField(field, id, updateData) {
const setClauses = [];
const values = [];
let i = 1;
for (const [key, value] of Object.entries(updateData)) {
if (value !== undefined) {
setClauses.push(`${key} = $${i}`);
values.push(value);
i++;
}
}
if (setClauses.length === 0) return;
values.push(id);
await pool.query(
`UPDATE alunos_cobrancas SET ${setClauses.join(', ')} WHERE ${field} = $${i}`,
values
);
}
// ============================================================
// HELPERS: provas_submissoes
// ============================================================
export async function getSubmissoesByAluno(alunoId) {
const { rows } = await pool.query(
'SELECT * FROM provas_submissoes WHERE aluno_id = $1',
[alunoId]
);
return rows;
}
export async function getSubmissaoByAlunoAndExam(alunoId, examId) {
const { rows } = await pool.query(
'SELECT id FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1',
[alunoId, examId]
);
return rows;
}
export async function insertSubmissao(submission) {
await pool.query(
`INSERT INTO provas_submissoes (aluno_id, prova_id, total_questoes, acertos, erros, percentual, nota_final, respostas, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
submission.aluno_id, submission.exam_id,
submission.total_questions, submission.correct_count, submission.wrong_count,
submission.percentage, submission.final_score,
JSON.stringify(submission.answers_json), submission.created_at
]
);
}
// ============================================================
// HELPERS: notas_boletim
// ============================================================
export async function initNotasTable() {
// Remover constraints restritivas da tabela de submissões se existirem (transição JSON -> Postgres)
try {
await pool.query(`
ALTER TABLE provas_submissoes DROP CONSTRAINT IF EXISTS provas_submissoes_aluno_id_fkey;
ALTER TABLE provas_submissoes DROP CONSTRAINT IF EXISTS provas_submissoes_prova_id_fkey;
`);
} catch (err) {
console.log('[PostgreSQL] Submissoes fkey já removidas ou tabela não existe.');
}
// Garantir unicidade do asaas_payment_id para permitir ON CONFLICT
try {
await pool.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'unique_asaas_payment_id') THEN
ALTER TABLE alunos_cobrancas ADD CONSTRAINT unique_asaas_payment_id UNIQUE (asaas_payment_id);
END IF;
END $$;
`);
} catch (err) {
console.warn('[PostgreSQL] Erro ao garantir UNIQUE em alunos_cobrancas:', err.message);
}
await pool.query(`
CREATE TABLE IF NOT EXISTS notas_boletim (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aluno_id VARCHAR(255) NOT NULL,
disciplina_id VARCHAR(255) NOT NULL,
periodo_id VARCHAR(255) NOT NULL,
prova_id VARCHAR(255),
valor NUMERIC(5, 2) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(aluno_id, disciplina_id, periodo_id, prova_id)
);
`);
}
export async function getNotasByAluno(alunoId) {
const { rows } = await pool.query(
'SELECT * FROM notas_boletim WHERE aluno_id = $1',
[alunoId]
);
return rows;
}
export async function upsertNota(nota) {
// Trata prova_id null se for direta para o unique index funcionar de forma previsível (PostgreSQL 15+ tem NULLS NOT DISTINCT, mas para garantir via app logic vamos usar uma abordagem de ON CONFLICT)
// No caso do PostgreSQL padrão, múltiplos NULLs não dão conflito no UNIQUE.
// Para contornar e permitir upsert real, faremos DELETE e INSERT ou garantiremos que o código gerencie o NULL logicamente.
if (nota.prova_id) {
await pool.query(
`INSERT INTO notas_boletim (aluno_id, disciplina_id, periodo_id, prova_id, valor, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (aluno_id, disciplina_id, periodo_id, prova_id)
DO UPDATE SET valor = EXCLUDED.valor, updated_at = NOW()`,
[nota.aluno_id, nota.disciplina_id, nota.periodo_id, nota.prova_id, nota.valor]
);
} else {
// Para notas diretas, se existir apagamos e inserimos (pois o unique index normal não restringe múltiplos nulls)
await pool.query(
`DELETE FROM notas_boletim WHERE aluno_id = $1 AND disciplina_id = $2 AND periodo_id = $3 AND prova_id IS NULL`,
[nota.aluno_id, nota.disciplina_id, nota.periodo_id]
);
await pool.query(
`INSERT INTO notas_boletim (aluno_id, disciplina_id, periodo_id, prova_id, valor, updated_at)
VALUES ($1, $2, $3, NULL, $4, NOW())`,
[nota.aluno_id, nota.disciplina_id, nota.periodo_id, nota.valor]
);
}
}
export async function deleteNotasManuaisAusentes(alunoId, notasManuaisRetidas) {
// Para limpar notas que o professor apagou (vazio) no manager
// notasManuaisRetidas é um array de objetos { disciplina_id, periodo_id, prova_id }
// Implementaremos a limpeza iterativamente na rota
}
// ============================================================
// SINCRONIZAÇÃO: JSON -> TABELAS RELACIONAIS
// Garante que IDs do JSON existam nas tabelas para evitar erro de Foreign Key
// ============================================================
export async function syncJsonToRelationalTables() {
const client = await pool.connect();
try {
const data = await getSchoolData();
if (!data) return;
console.log('[Sincronização] 🔄 Iniciando espelhamento TOTAL (Modo Blindado)...');
await client.query('BEGIN');
// Garantir colunas na tabela frequencias
await client.query(`ALTER TABLE frequencias ADD COLUMN IF NOT EXISTS justificativa TEXT`);
await client.query(`ALTER TABLE frequencias ADD COLUMN IF NOT EXISTS justificativa_aceita BOOLEAN DEFAULT FALSE`);
// 1. Sincronizar Cursos
if (data.courses && Array.isArray(data.courses)) {
const courseIds = data.courses.map(c => c.id).filter(Boolean);
if (courseIds.length > 0) {
await client.query('DELETE FROM cursos WHERE id != ALL($1)', [courseIds]);
} else {
await client.query('DELETE FROM cursos');
}
for (const c of data.courses) {
if (!c.id || !c.name) continue;
await client.query(
`INSERT INTO cursos (id, nome, duracao, duracao_meses, taxa_matricula, mensalidade, descricao, multa_percentual, juros_percentual)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, duracao = EXCLUDED.duracao, duracao_meses = EXCLUDED.duracao_meses,
taxa_matricula = EXCLUDED.taxa_matricula, mensalidade = EXCLUDED.mensalidade,
descricao = EXCLUDED.descricao, multa_percentual = EXCLUDED.multa_percentual,
juros_percentual = EXCLUDED.juros_percentual`,
[c.id, c.name, c.duration || '', c.durationMonths || 0, c.registrationFee || 0, c.monthlyFee || 0, c.description || '', c.finePercentage || 0, c.interestPercentage || 0]
);
}
}
// Garantir colunas de refação em provas
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS permitir_refacao BOOLEAN DEFAULT FALSE');
// 2. Sincronizar Disciplinas (Subjects)
if (data.subjects && Array.isArray(data.subjects)) {
const subIds = data.subjects.map(s => s.id).filter(Boolean);
if (subIds.length > 0) {
await client.query('DELETE FROM disciplinas WHERE id != ALL($1)', [subIds]);
} else {
await client.query('DELETE FROM disciplinas');
}
for (const sub of data.subjects) {
if (!sub.id || !sub.name) continue;
await client.query(
`INSERT INTO disciplinas (id, nome)
VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
[sub.id, sub.name]
);
}
}
// 0. Garantir esquema atualizado
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS valor_pago NUMERIC(10,2) DEFAULT 0');
// 1. Sincronizar Perfil da Escola (Configurações)
if (data.periods && Array.isArray(data.periods)) {
const periodIds = data.periods.map(p => p.id).filter(Boolean);
if (periodIds.length > 0) {
await client.query('DELETE FROM periodos WHERE id != ALL($1)', [periodIds]);
} else {
await client.query('DELETE FROM periodos');
}
for (const p of data.periods) {
if (!p.id || !p.name) continue;
await client.query(
`INSERT INTO periodos (id, nome)
VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
[p.id, p.name]
);
}
}
// 4. Sincronizar Turmas
if (data.classes && Array.isArray(data.classes)) {
const classIds = data.classes.map(t => t.id).filter(Boolean);
if (classIds.length > 0) {
await client.query('DELETE FROM turmas WHERE id != ALL($1)', [classIds]);
} else {
await client.query('DELETE FROM turmas');
}
for (const t of data.classes) {
if (!t.id || !t.name) continue;
await client.query(
`INSERT INTO turmas (id, nome, curso_id, professor, horario, dia_semana, max_alunos, data_inicio, data_fim, horario_inicio_padrao, horario_fim_padrao)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, curso_id = EXCLUDED.curso_id, professor = EXCLUDED.professor,
horario = EXCLUDED.horario, dia_semana = EXCLUDED.dia_semana, max_alunos = EXCLUDED.max_alunos,
data_inicio = EXCLUDED.data_inicio, data_fim = EXCLUDED.data_fim,
horario_inicio_padrao = EXCLUDED.horario_inicio_padrao, horario_fim_padrao = EXCLUDED.horario_fim_padrao`,
[t.id, t.name, t.courseId || null, t.teacher || '', t.schedule || '', t.scheduleDay || null, t.maxStudents || 30, t.startDate || null, t.endDate || null, t.defaultStartTime || null, t.defaultEndTime || null]
);
}
}
// 5. Sincronizar Alunos
if (data.students && Array.isArray(data.students)) {
const studentIds = data.students.map(s => s.id).filter(Boolean);
if (studentIds.length > 0) {
await client.query('DELETE FROM alunos WHERE id != ALL($1)', [studentIds]);
} else {
await client.query('DELETE FROM alunos');
}
for (const s of data.students) {
if (!s.id || !s.name) continue;
await client.query(
`INSERT INTO alunos (
id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao,
nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel,
turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado,
desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, email = EXCLUDED.email, telefone = EXCLUDED.telefone, data_nascimento = EXCLUDED.data_nascimento,
cpf = EXCLUDED.cpf, rg = EXCLUDED.rg, rg_data_emissao = EXCLUDED.rg_data_emissao,
nome_responsavel = EXCLUDED.nome_responsavel, telefone_responsavel = EXCLUDED.telefone_responsavel,
cpf_responsavel = EXCLUDED.cpf_responsavel, data_nascimento_responsavel = EXCLUDED.data_nascimento_responsavel,
turma_id = EXCLUDED.turma_id, status = EXCLUDED.status, data_matricula = EXCLUDED.data_matricula,
foto_url = EXCLUDED.foto_url, cep = EXCLUDED.cep, rua = EXCLUDED.rua, numero = EXCLUDED.numero,
bairro = EXCLUDED.bairro, cidade = EXCLUDED.cidade, estado = EXCLUDED.estado,
desconto = EXCLUDED.desconto, tem_responsavel = EXCLUDED.tem_responsavel,
modelo_contrato_id = EXCLUDED.modelo_contrato_id, numero_matricula = EXCLUDED.numero_matricula,
senha_portal = EXCLUDED.senha_portal`,
[
s.id, s.name, s.email || '', s.phone || '', s.birthDate || null, s.cpf || '', s.rg || '', s.rgIssueDate || null,
s.guardianName || '', s.guardianPhone || '', s.guardianCpf || '', s.guardianBirthDate || null,
s.classId || null, s.status || 'active', s.registrationDate || null, s.photo || '',
s.addressZip || '', s.addressStreet || '', s.addressNumber || '', s.addressNeighborhood || '', s.addressCity || '', s.addressState || '',
s.discount || 0, s.hasGuardian || false, s.contractTemplateId || null, s.enrollmentNumber || null, s.portalPassword || null
]
);
}
}
// 6. Sincronizar Provas
if (data.exams && Array.isArray(data.exams)) {
const examIds = data.exams.map(e => e.id).filter(Boolean);
if (examIds.length > 0) {
await client.query('DELETE FROM provas WHERE id != ALL($1)', [examIds]);
} else {
await client.query('DELETE FROM provas');
}
for (const e of data.exams) {
if (!e.id || !e.title) continue;
await client.query(
`INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status, permitir_refacao)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
turma_id = EXCLUDED.turma_id, disciplina_id = EXCLUDED.disciplina_id, periodo_id = EXCLUDED.periodo_id,
titulo = EXCLUDED.titulo, duracao_minutos = EXCLUDED.duracao_minutos, status = EXCLUDED.status,
permitir_refacao = EXCLUDED.permitir_refacao`,
[e.id, e.classId || null, e.subjectId || null, e.periodId || null, e.title, e.durationMinutes || 60, e.status || 'draft', e.allowRetake || false]
).catch(err => console.warn(`[Sync:Provas] Erro na prova ${e.id}:`, err.message));
}
}
// 7. Sincronizar Frequências
if (data.attendance && Array.isArray(data.attendance)) {
const attIds = data.attendance.map(f => f.id).filter(Boolean);
if (attIds.length > 0) {
await client.query('DELETE FROM frequencias WHERE id != ALL($1)', [attIds]);
} else {
await client.query('DELETE FROM frequencias');
}
for (const f of data.attendance) {
if (!f.id || !f.studentId || !f.classId) continue;
await client.query(
`INSERT INTO frequencias (id, aluno_id, turma_id, data, foto, verificado, tipo, justificativa, justificativa_aceita)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
aluno_id = EXCLUDED.aluno_id, turma_id = EXCLUDED.turma_id, data = EXCLUDED.data,
foto = EXCLUDED.foto, verificado = EXCLUDED.verificado, tipo = EXCLUDED.tipo,
justificativa = EXCLUDED.justificativa, justificativa_aceita = EXCLUDED.justificativa_aceita`,
[f.id, f.studentId, f.classId, f.date, f.photo || '', f.verified || false, f.type || 'presence', f.justification || null, f.justificationAccepted || false]
);
}
}
// 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';
const isPaid = sqlStatus === 'PAGO';
const amount = Number(p.amount || 0);
const discount = Number(p.discount || 0);
// O valor da parcela (face value) é sempre o amount do JSON (ex: 170)
// O desconto é condicional e NÃO altera o valor base da parcela
let valorBruto = amount;
let valorPago = 0;
if (isPaid) {
// Usar valor_pago explícito do JSON se disponível, senão calcular (amount - discount)
valorPago = Number(p.valor_pago || 0) || (amount - discount);
}
await client.query(
`INSERT INTO alunos_cobrancas (
aluno_id, asaas_payment_id, asaas_installment_id, installment,
valor, vencimento, link_boleto, status,
description, type, discount, installment_number, total_installments,
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)
ON CONFLICT (asaas_payment_id) DO UPDATE SET
aluno_id = EXCLUDED.aluno_id,
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 = 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),
valor_pago = EXCLUDED.valor_pago`,
[
p.studentId,
p.asaasPaymentId,
p.asaasInstallmentId || p.installmentId || null,
p.installment || null,
valorBruto,
p.dueDate,
p.bankSlipUrl || p.link || null,
sqlStatus,
p.description || null,
p.type || 'monthly',
discount,
p.installmentNumber || null,
p.totalInstallments || null,
p.contractId || null,
p.asaasPaymentUrl || null,
valorBruto,
p.paidDate || null,
valorPago
]
).catch(err => console.warn(`[Sync:Finance] Erro no boleto ${p.asaasPaymentId}:`, err.message));
}
}
await client.query('COMMIT');
console.log('[Sincronização] 🚀 Espelhamento TOTAL concluído com sucesso!');
} catch (err) {
await client.query('ROLLBACK');
console.error('[Sincronização] ❌ Erro Crítico:', err.message);
} finally {
client.release();
}
}
// ============================================================
// EXPORT POOL para queries diretas quando necessário
// ============================================================
export { pool };
export default pool;