/** * ============================================================ * 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); // Se está pago, o 'amount' do JSON geralmente é o líquido. // O valor principal (valor) deve ser o BRUTO. let valorBruto = amount; let valorPago = 0; if (isPaid) { valorPago = amount; // Se o amount vindo do JSON for o líquido (igual ou menor que o bruto esperado), restauramos o bruto valorBruto = 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 = GREATEST(alunos_cobrancas.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 = GREATEST(COALESCE(alunos_cobrancas.amount_original, 0), EXCLUDED.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;