634 lines
26 KiB
JavaScript
634 lines
26 KiB
JavaScript
/**
|
||
* ============================================================
|
||
* 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;
|