/** * ============================================================ * 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; } // Cleanup SQL orphaned absences as well, so SQL and JSON are consistently cleaned try { const client = await pool.connect(); try { await client.query(` DELETE FROM frequencias a WHERE a.tipo = 'absence' AND a.id LIKE 'auto-abs-%' AND EXISTS ( SELECT 1 FROM frequencias p WHERE p.tipo = 'presence' AND p.aluno_id = a.aluno_id AND (p.aula_id = a.aula_id OR DATE(p.data AT TIME ZONE 'UTC') = DATE(a.data AT TIME ZONE 'UTC')) ) `); } finally { client.release(); } } catch (err) { console.warn('[Database:AutoAbsences] Erro ao limpar faltas órfãs no SQL:', err.message); } // 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 created_at 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.map(r => ({ id: r.id, name: r.nome, cpf: r.cpf, email: r.email, phone: r.telefone, categoryId: r.categoria_id, hireDate: r.data_admissao, createdAt: r.created_at })); } export async function getCategoriasFuncionarios() { const { rows } = await pool.query('SELECT * FROM categorias_funcionarios ORDER BY nome ASC'); return rows.map(r => ({ id: r.id, name: r.nome, createdAt: r.created_at })); } 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.map(r => ({ ...r, classId: r.turma_id, name: r.nome, status: r.status, cpf: r.cpf, phone: r.telefone, registrationDate: r.data_matricula, contractTemplateId: r.modelo_contrato_id, faceDescriptor: r.face_descriptor })); } 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, face_descriptor ) 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, $29 ) 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, a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : 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, face_descriptor=COALESCE($28, face_descriptor) WHERE id = $29 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, a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : 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 updateContrato(id, c) { await pool.query('UPDATE contratos SET titulo=$1, conteudo=$2 WHERE id=$3', [c.title, c.content, id]); } 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]); } // ============================================================ // FREQUÊNCIAS (CHAMADA) // ============================================================ export async function getFrequencias() { const { rows } = await pool.query(` SELECT *, TO_CHAR(data, 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data FROM frequencias ORDER BY created_at DESC `); return rows.map(r => ({ id: r.id, studentId: r.aluno_id, classId: r.turma_id, lessonId: r.aula_id, date: r.formatted_data || r.data, photo: r.foto_url || r.foto, verified: r.verificado, type: r.tipo, justification: r.justificativa, justificationAccepted: r.justificativa_aceita, createdAt: r.created_at })); } export async function insertFrequencia(f) { await pool.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)`, [ f.id, f.studentId, f.classId, f.lessonId, f.date, f.photo, f.verified || false, f.type || 'presence', f.justification, f.justificationAccepted || false ] ); } export async function updateFrequencia(id, f) { // Update apenas dos campos que podem mudar await pool.query( `UPDATE frequencias SET tipo = $1, justificativa = $2, justificativa_aceita = $3, verificado = $4 WHERE id = $5`, [f.type, f.justification, f.justificationAccepted, f.verified, id] ); } export async function deleteFrequencia(id) { await pool.query('DELETE FROM frequencias WHERE id = $1', [id]); } // ============================================================ // PROVAS & QUESTÕES (FASE 5) // ============================================================ export async function getProvas() { const { rows: provasRows } = await pool.query('SELECT * FROM provas ORDER BY created_at DESC'); // Mapear campos para camelCase e buscar questoes para compatibilidade com o frontend antigo const provasFormatadas = []; for (const p of provasRows) { const { rows: questoesRows } = await pool.query('SELECT * FROM questoes_provas WHERE prova_id = $1 ORDER BY ordem ASC', [p.id]); provasFormatadas.push({ id: p.id, classId: p.turma_id, subjectId: p.disciplina_id, periodId: p.periodo_id, title: p.titulo, durationMinutes: p.duracao_minutos, status: p.status, allowRetake: p.permitir_refacao, isDeleted: p.is_deleted, evaluationType: p.evaluation_type || 'exam', questions: questoesRows.map(q => ({ id: q.id, examId: q.prova_id, text: q.texto, options: q.opcoes || [], correctAnswer: q.indice_correto, order: q.ordem, imageUrl: q.imagem_url })) }); } return provasFormatadas; } 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 // [REMOVIDO] A tabela de frequencias agora é a Single Source of Truth (SQL-First). // O JSON legado data.attendance é ignorado para não sobrescrever os registros reais do banco. // 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;