edumanagerpro2/manager/services/database.js

1227 lines
49 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';
// Registrar parser global para tipo NUMERIC (OID 1700) para retornar como Number
pg.types.setTypeParser(1700, (val) => val === null ? null : parseFloat(val));
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;
// 1. Auto-correção/Autolimpeza: remove faltas auto-geradas duplicadas se o aluno tiver uma presença registrada no mesmo dia/aula
const initialLength = data.attendance.length;
data.attendance = data.attendance.filter(a => {
if (a.type === 'absence' && (a.autoGenerated || (a.id && a.id.startsWith('auto-abs-')))) {
const hasPresence = data.attendance.some(p =>
p.studentId === a.studentId &&
(p.type === 'presence' || !p.type) &&
(p.lessonId === a.lessonId || (p.date && a.date && p.date.substring(0, 10) === a.date.substring(0, 10)))
);
return !hasPresence;
}
return true;
});
if (data.attendance.length !== initialLength) {
updated = true;
}
// Cache de alunos por turma para performance
const studentsByClass = {};
data.lessons.forEach(lesson => {
// RESOLUÇÃO DE FUSO HORÁRIO: Força fuso horário América/São_Paulo (-03:00) ao analisar a hora de término da aula no servidor (geralmente UTC)
const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00-03: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]
);
}
// --- SYNC MODELOS CONTRATO ---
if (schoolData.contractTemplates && schoolData.contractTemplates.length > 0) {
for (const t of schoolData.contractTemplates) {
await client.query(
`INSERT INTO modelos_contrato (id, nome, conteudo) VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET nome=EXCLUDED.nome, conteudo=EXCLUDED.conteudo`,
[t.id, t.name, t.content]
).catch(err => console.warn(`[Sync:Modelos] Erro ${t.id}:`, err.message));
}
}
// --- SYNC CONTRATOS ---
if (schoolData.contracts && schoolData.contracts.length > 0) {
for (const c of schoolData.contracts) {
await client.query(
`INSERT INTO contratos (id, aluno_id, titulo, conteudo, created_at) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET titulo=EXCLUDED.titulo, conteudo=EXCLUDED.conteudo`,
[c.id, c.studentId, c.title, c.content, c.createdAt]
).catch(err => console.warn(`[Sync:Contratos] Erro ${c.id}:`, err.message));
}
}
// --- SYNC AULAS ---
if (schoolData.lessons && schoolData.lessons.length > 0) {
for (const a of schoolData.lessons) {
await client.query(
`INSERT INTO aulas (
id, turma_id, data, horario_inicio, horario_fim, status, tipo, motivo_cancelamento, aula_original_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
turma_id = EXCLUDED.turma_id,
data = EXCLUDED.data,
horario_inicio = COALESCE(EXCLUDED.horario_inicio, aulas.horario_inicio),
horario_fim = COALESCE(EXCLUDED.horario_fim, aulas.horario_fim),
status = EXCLUDED.status,
tipo = EXCLUDED.tipo,
motivo_cancelamento = EXCLUDED.motivo_cancelamento,
aula_original_id = COALESCE(EXCLUDED.aula_original_id, aulas.aula_original_id)`,
[
a.id, a.classId, a.date, a.startTime || null, a.endTime || null, a.status || 'scheduled', a.type || 'regular',
a.cancelReason || null, a.originalLessonId || null
]
).catch(err => console.warn(`[Sync:Aulas] Erro na aula ${a.id}:`, err.message));
}
}
// --- SYNC FREQUENCIAS ---
if (schoolData.attendance && schoolData.attendance.length > 0) {
for (const f of schoolData.attendance) {
await client.query(
`INSERT INTO frequencias (
id, aula_id, turma_id, aluno_id, tipo, data_registro, url_anexo, justificado
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
tipo = EXCLUDED.tipo,
url_anexo = COALESCE(EXCLUDED.url_anexo, frequencias.url_anexo),
justificado = EXCLUDED.justificado`,
[
f.id, f.lessonId, f.classId, f.studentId, f.type, f.date, f.attachment || null, f.justified || false
]
).catch(err => console.warn(`[Sync:Freq] Erro na freq ${f.id}:`, err.message));
}
}
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
}
// ============================================================
// HELPERS: cursos e turmas
// ============================================================
export async function getCursos() {
const { rows } = await pool.query('SELECT * FROM cursos ORDER BY nome ASC');
return rows;
}
export async function insertCurso(c) {
await pool.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)`,
[c.id, c.nome, c.duracao || '', c.duracao_meses || 12, c.taxa_matricula || 0, c.mensalidade || 0, c.descricao || '', c.multa_percentual || 0, c.juros_percentual || 0]
);
}
export async function updateCurso(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 cursos SET ${setClauses.join(', ')} WHERE id = $${i}`,
values
);
}
export async function deleteCurso(id) {
await pool.query('DELETE FROM cursos WHERE id = $1', [id]);
}
export async function getTurmas() {
const { rows } = await pool.query('SELECT * FROM turmas ORDER BY nome ASC');
return rows;
}
export async function insertTurma(t) {
await pool.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)`,
[t.id, t.nome, t.curso_id || null, t.professor || '', t.horario || '', t.dia_semana || null, t.max_alunos || 30, t.data_inicio || null, t.data_fim || null, t.horario_inicio_padrao || null, t.horario_fim_padrao || null]
);
}
export async function updateTurma(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 turmas SET ${setClauses.join(', ')} WHERE id = $${i}`,
values
);
}
export async function deleteTurma(id) {
await pool.query('DELETE FROM turmas WHERE id = $1', [id]);
}
export async function getDisciplinas() {
const { rows } = await pool.query('SELECT * FROM disciplinas ORDER BY nome ASC');
return rows;
}
export async function insertDisciplina(d) {
await pool.query(
`INSERT INTO disciplinas (id, nome) VALUES ($1, $2)`,
[d.id, d.nome]
);
}
export async function updateDisciplina(id, updateData) {
if (updateData.nome !== undefined) {
await pool.query(
`UPDATE disciplinas SET nome = $1 WHERE id = $2`,
[updateData.nome, id]
);
}
}
export async function deleteDisciplina(id) {
await pool.query('DELETE FROM disciplinas WHERE id = $1', [id]);
}
// ============================================================
// HELPERS: funcionarios e categorias_funcionarios
// ============================================================
export async function getFuncionarios() {
const { rows } = await pool.query('SELECT * FROM funcionarios ORDER BY nome ASC');
return rows;
}
export async function getCategoriasFuncionarios() {
const { rows } = await pool.query('SELECT * FROM categorias_funcionarios ORDER BY nome ASC');
return rows;
}
export async function insertFuncionario(f) {
await pool.query(
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[f.id, f.nome, f.cpf, f.email, f.telefone, f.categoria_id, f.data_admissao || null]
);
}
export async function updateFuncionario(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 funcionarios SET ${setClauses.join(', ')} WHERE id = $${i}`,
values
);
}
export async function deleteFuncionario(id) {
await pool.query('DELETE FROM funcionarios WHERE id = $1', [id]);
}
export async function insertCategoriaFuncionario(c) {
await pool.query(
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)`,
[c.id, c.nome]
);
}
export async function updateCategoriaFuncionario(id, nome) {
await pool.query(
`UPDATE categorias_funcionarios SET nome = $1 WHERE id = $2`,
[nome, id]
);
}
export async function deleteCategoriaFuncionario(id) {
await pool.query('DELETE FROM categorias_funcionarios WHERE id = $1', [id]);
}
// ============================================================
// ALUNOS (FASE 4)
// ============================================================
export async function getAlunos() {
const result = await pool.query("SELECT * FROM alunos ORDER BY nome ASC");
return result.rows;
}
export async function insertAluno(a) {
const result = await pool.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,
motivo_cancelamento
) 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, $28
) RETURNING *`,
[
a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null,
a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null,
a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '',
a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null,
a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null,
a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '',
a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '',
a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false),
a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null,
a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null
]
);
return result.rows[0];
}
export async function updateAluno(id, a) {
const result = await pool.query(
`UPDATE alunos SET
nome=$1, email=$2, telefone=$3, data_nascimento=$4, cpf=$5, rg=$6, rg_data_emissao=$7,
nome_responsavel=$8, telefone_responsavel=$9, cpf_responsavel=$10, data_nascimento_responsavel=$11,
turma_id=$12, status=$13, data_matricula=$14, foto_url=$15, cep=$16, rua=$17, numero=$18, bairro=$19, cidade=$20, estado=$21,
desconto=$22, tem_responsavel=$23, modelo_contrato_id=$24, numero_matricula=$25, senha_portal=$26,
motivo_cancelamento=$27
WHERE id = $28 RETURNING *`,
[
a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null,
a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null,
a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '',
a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null,
a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null,
a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '',
a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '',
a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false),
a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null,
a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null,
id
]
);
return result.rows[0];
}
export async function deleteAluno(id) {
await pool.query('DELETE FROM alunos WHERE id = $1', [id]);
}
// ============================================================
// CONTRATOS E MODELOS
// ============================================================
export async function getModelosContrato() {
const { rows } = await pool.query('SELECT * FROM modelos_contrato ORDER BY nome ASC');
return rows.map(r => ({ id: r.id, name: r.nome, content: r.conteudo }));
}
export async function insertModeloContrato(m) {
await pool.query('INSERT INTO modelos_contrato (id, nome, conteudo) VALUES ($1, $2, $3)', [m.id, m.name, m.content]);
}
export async function updateModeloContrato(id, m) {
await pool.query('UPDATE modelos_contrato SET nome=$1, conteudo=$2 WHERE id=$3', [m.name, m.content, id]);
}
export async function deleteModeloContrato(id) {
await pool.query('DELETE FROM modelos_contrato WHERE id=$1', [id]);
}
export async function getContratos() {
const { rows } = await pool.query('SELECT *, TO_CHAR(created_at, \'YYYY-MM-DD"T"HH24:MI:SS"Z"\') as created_at_fmt FROM contratos ORDER BY created_at DESC');
return rows.map(r => ({ id: r.id, studentId: r.aluno_id, title: r.titulo, content: r.conteudo, createdAt: r.created_at_fmt }));
}
export async function insertContrato(c) {
await pool.query('INSERT INTO contratos (id, aluno_id, titulo, conteudo) VALUES ($1, $2, $3, $4)', [c.id, c.studentId, c.title, c.content]);
}
export async function deleteContrato(id) {
await pool.query('DELETE FROM contratos WHERE id=$1', [id]);
}
// ============================================================
// AULAS E CRONOGRAMA
// ============================================================
export async function getAulasByTurma(turma_id) {
const result = await pool.query(
`SELECT *, TO_CHAR(data, 'YYYY-MM-DD') as data_formatada FROM aulas WHERE turma_id = $1 ORDER BY data ASC, horario_inicio ASC`,
[turma_id]
);
return result.rows.map(row => ({
id: row.id,
classId: row.turma_id,
date: row.data_formatada,
startTime: row.horario_inicio,
endTime: row.horario_fim,
status: row.status,
type: row.tipo,
cancellationReason: row.motivo_cancelamento,
originalLessonId: row.aula_original_id
}));
}
export async function getAllAulas() {
const result = await pool.query(
`SELECT *, TO_CHAR(data, 'YYYY-MM-DD') as data_formatada FROM aulas ORDER BY data ASC, horario_inicio ASC`
);
return result.rows.map(row => ({
id: row.id,
classId: row.turma_id,
date: row.data_formatada,
startTime: row.horario_inicio,
endTime: row.horario_fim,
status: row.status,
type: row.tipo,
cancellationReason: row.motivo_cancelamento,
originalLessonId: row.aula_original_id
}));
}
export async function insertAulas(aulas) {
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const a of aulas) {
await client.query(
`INSERT INTO aulas (
id, turma_id, data, horario_inicio, horario_fim, status, tipo, motivo_cancelamento, aula_original_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
turma_id=$2, data=$3, horario_inicio=$4, horario_fim=$5, status=$6, tipo=$7, motivo_cancelamento=$8, aula_original_id=$9`,
[
a.id, a.classId, a.date, a.startTime, a.endTime, a.status || 'scheduled', a.type || 'regular',
a.cancellationReason || null, a.originalLessonId || null
]
);
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
export async function deleteAulas(ids) {
if (!ids || ids.length === 0) return;
await pool.query('DELETE FROM aulas WHERE id = ANY($1)', [ids]);
}
// ============================================================
// PROVAS & QUESTÕES (FASE 5)
// ============================================================
export async function getProvas() {
const result = await pool.query('SELECT * FROM provas ORDER BY created_at DESC');
return result.rows;
}
export async function getQuestoesDaProva(provaId) {
const result = await pool.query('SELECT * FROM questoes_provas WHERE prova_id = $1 ORDER BY ordem ASC', [provaId]);
return result.rows;
}
export async function insertProva(p) {
await pool.query(
`INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status, permitir_refacao, is_deleted, evaluation_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
p.id, p.turma_id || p.classId, p.disciplina_id || p.subjectId, p.periodo_id || p.periodId,
p.titulo || p.title, p.duracao_minutos || p.durationMinutes || 60, p.status || 'draft',
p.permitir_refacao || p.allowRetake || false,
p.is_deleted || p.isDeleted || false,
p.evaluation_type || p.evaluationType || 'exam'
]
);
}
export async function updateProva(id, p) {
await pool.query(
`UPDATE provas SET
turma_id = $1, disciplina_id = $2, periodo_id = $3, titulo = $4, duracao_minutos = $5, status = $6, permitir_refacao = $7, is_deleted = $8, evaluation_type = $9
WHERE id = $10`,
[
p.turma_id || p.classId, p.disciplina_id || p.subjectId, p.periodo_id || p.periodId,
p.titulo || p.title, p.duracao_minutos || p.durationMinutes || 60, p.status || 'draft',
p.permitir_refacao || p.allowRetake || false,
p.is_deleted || p.isDeleted || false,
p.evaluation_type || p.evaluationType || 'exam',
id
]
);
}
export async function deleteProva(id) {
await pool.query('DELETE FROM provas WHERE id = $1', [id]);
}
export async function syncQuestoesProva(provaId, questoes) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM questoes_provas WHERE prova_id = $1', [provaId]);
for (let i = 0; i < questoes.length; i++) {
const q = questoes[i];
await client.query(
`INSERT INTO questoes_provas (id, prova_id, texto, imagem_url, opcoes, indice_correto, ordem)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[q.id || require('crypto').randomUUID(), provaId, q.texto || q.text, q.imagem_url || q.imageUrl, JSON.stringify(q.opcoes || q.options || []), q.indice_correto ?? q.correctIndex ?? 0, i]
);
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
// ============================================================
// 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]
);
}
}
// 1.5 Sincronizar Categorias de Funcionários
if (data.employeeCategories && Array.isArray(data.employeeCategories)) {
for (const cat of data.employeeCategories) {
if (!cat.id || !cat.name) continue;
await client.query(
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
[cat.id, cat.name]
);
}
}
// 1.6 Sincronizar Funcionários
if (data.employees && Array.isArray(data.employees)) {
for (const emp of data.employees) {
if (!emp.id || !emp.name) continue;
await client.query(
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, cpf = EXCLUDED.cpf, email = EXCLUDED.email,
telefone = EXCLUDED.telefone, categoria_id = EXCLUDED.categoria_id,
data_admissao = EXCLUDED.data_admissao`,
[emp.id, emp.name, emp.cpf || '', emp.email || '', emp.phone || '', emp.categoryId || null, emp.admissionDate || null]
);
}
}
// Garantir colunas de refação e soft delete em provas
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS permitir_refacao BOOLEAN DEFAULT FALSE');
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE');
await client.query("ALTER TABLE provas ADD COLUMN IF NOT EXISTS evaluation_type VARCHAR(50) DEFAULT 'exam'");
// 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');
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)
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, aula_id, data, foto, verificado, tipo, justificativa, justificativa_aceita)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
aluno_id = EXCLUDED.aluno_id, turma_id = EXCLUDED.turma_id, aula_id = EXCLUDED.aula_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.lessonId || null, 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.studentId || !p.id) 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);
}
if (p.asaasPaymentId) {
// Cobrança vinculada ao Asaas (usa ON CONFLICT em asaas_payment_id)
await client.query(
`INSERT INTO alunos_cobrancas (
local_id, 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, $19)
ON CONFLICT (asaas_payment_id) DO UPDATE SET
local_id = COALESCE(EXCLUDED.local_id, alunos_cobrancas.local_id),
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.id,
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 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));
}
}
}
}
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;