edumanagerpro2/manager/migrate_to_local.ts

556 lines
24 KiB
TypeScript

/**
* ============================================================
* SCRIPT DE MIGRAÇÃO: SUPABASE CLOUD → POSTGRESQL LOCAL
* ============================================================
*
* COMO USAR:
* 1. Certifique-se de que o PostgreSQL local está rodando (docker-compose up postgres)
* 2. Instale as dependências: npm install pg @supabase/supabase-js dotenv
* 3. Configure as variáveis de ambiente no arquivo .env.migration
* 4. Execute: npx tsx migrate_to_local.ts
*
* IMPORTANTE:
* - Este script NÃO altera nada no Supabase. Ele apenas LÊ.
* - Senhas são copiadas EXATAMENTE como estão, sem rehash.
* - O script usa transações atômicas: se falhar no meio, nada é salvo.
*/
import { createClient } from '@supabase/supabase-js';
import pg from 'pg';
import fs from 'fs';
// ============================================================
// CONFIGURAÇÃO — Altere aqui ou use .env.migration
// ============================================================
const SUPABASE_URL = process.env.VITE_SUPABASE_URL || 'https://ekbuvcjsfcczviqqlfit.supabase.co';
const SUPABASE_KEY = process.env.VITE_SUPABASE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVrYnV2Y2pzZmNjenZpcXFsZml0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5OTU0MzIsImV4cCI6MjA4NjU3MTQzMn0.oIzBeGF-PjaviZejYb1TeOOEzMm-Jjth1XzvJrjD6us';
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager';
// ============================================================
// INICIALIZAÇÃO
// ============================================================
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const pool = new pg.Pool({ connectionString: DATABASE_URL });
function log(emoji: string, msg: string) {
console.log(`${emoji} ${msg}`);
}
function logCount(table: string, count: number) {
log('📦', `${table}: ${count} registro(s) migrado(s)`);
}
// ============================================================
// FUNÇÕES DE MIGRAÇÃO POR ENTIDADE
// ============================================================
async function migrateConfiguracoes(client: pg.PoolClient, schoolData: any) {
const profile = schoolData.profile || {};
const evoConfig = schoolData.evolutionConfig || {};
const msgTemplates = schoolData.messageTemplates || {};
await client.query(`
INSERT INTO configuracoes (id, nome, endereco, cidade, estado, cep, cnpj, telefone, email, tipo, logo, evolution_api_url, evolution_instance_name, evolution_api_key, message_templates)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, endereco = EXCLUDED.endereco, cidade = EXCLUDED.cidade,
estado = EXCLUDED.estado, cep = EXCLUDED.cep, cnpj = EXCLUDED.cnpj,
telefone = EXCLUDED.telefone, email = EXCLUDED.email, tipo = EXCLUDED.tipo,
logo = EXCLUDED.logo, evolution_api_url = EXCLUDED.evolution_api_url,
evolution_instance_name = EXCLUDED.evolution_instance_name,
evolution_api_key = EXCLUDED.evolution_api_key,
message_templates = EXCLUDED.message_templates
`, [
profile.id || 'main-school',
profile.name || 'EduManager School',
profile.address || '',
profile.city || '',
profile.state || '',
profile.zip || '',
profile.cnpj || '',
profile.phone || '',
profile.email || '',
profile.type || 'matriz',
schoolData.logo || '',
evoConfig.apiUrl || null,
evoConfig.instanceName || null,
evoConfig.apiKey || null,
JSON.stringify(msgTemplates)
]);
logCount('configuracoes', 1);
}
async function migrateUsuarios(client: pg.PoolClient, users: any[]) {
for (const u of users) {
await client.query(`
INSERT INTO usuarios (id, username, display_name, photo_url, password, cpf, role)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
username = EXCLUDED.username, display_name = EXCLUDED.display_name,
password = EXCLUDED.password, cpf = EXCLUDED.cpf, role = EXCLUDED.role
`, [u.id, u.name, u.displayName || null, u.photoURL || null, u.password, u.cpf || '', u.role || 'admin']);
}
logCount('usuarios', users.length);
}
async function migrateCursos(client: pg.PoolClient, courses: any[]) {
for (const c of courses) {
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
`, [c.id, c.name, c.duration || '', c.durationMonths || 0, c.registrationFee || 0, c.monthlyFee || 0, c.description || '', c.finePercentage || 0, c.interestPercentage || 0]);
}
logCount('cursos', courses.length);
}
async function migrateTurmas(client: pg.PoolClient, classes: any[]) {
for (const c of classes) {
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
`, [
c.id, c.name, c.courseId || null, c.teacher || '', c.schedule || '',
c.scheduleDay || null, c.maxStudents || 30,
c.startDate || null, c.endDate || null,
c.defaultStartTime || null, c.defaultEndTime || null
]);
}
logCount('turmas', classes.length);
}
async function migrateAlunos(client: pg.PoolClient, students: any[]) {
for (const s of students) {
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, motivo_cancelamento, data_matricula, foto_url, face_descriptor,
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,
$28, $29
)
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, email = EXCLUDED.email, telefone = EXCLUDED.telefone,
turma_id = EXCLUDED.turma_id, status = EXCLUDED.status,
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 || null, s.rgIssueDate || null,
s.guardianName || null, s.guardianPhone || null, s.guardianCpf || null, s.guardianBirthDate || null,
s.classId || null, s.status || 'active', s.cancellationReason || null,
s.registrationDate || null,
// FOTO: Copia a URL (se já migrou para Storage) ou o base64 temporariamente
s.photo || null,
// FACE DESCRIPTOR: Array de números para reconhecimento facial
s.faceDescriptor ? JSON.stringify(s.faceDescriptor) : null,
s.addressZip || '', s.addressStreet || '', s.addressNumber || '',
s.addressNeighborhood || '', s.addressCity || '', s.addressState || '',
s.discount || 0, s.hasGuardian || false, s.contractTemplateId || null,
// CRÍTICO: Matrícula e Senha copiadas EXATAMENTE como estão
s.enrollmentNumber || null, s.portalPassword || null
]);
}
logCount('alunos', students.length);
}
async function migrateAulas(client: pg.PoolClient, lessons: any[]) {
for (const l of 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
status = EXCLUDED.status, horario_inicio = EXCLUDED.horario_inicio,
horario_fim = EXCLUDED.horario_fim
`, [
l.id, l.classId, l.date, l.startTime || null, l.endTime || null,
l.status || 'scheduled', l.type || 'regular',
l.cancelReason || null, l.originalLessonId || null
]);
}
logCount('aulas', lessons.length);
}
async function migrateFrequencias(client: pg.PoolClient, attendance: any[]) {
for (const a of attendance) {
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
tipo = EXCLUDED.tipo, justificativa = EXCLUDED.justificativa,
justificativa_aceita = EXCLUDED.justificativa_aceita
`, [
a.id, a.studentId, a.classId, a.date,
a.photo || null, a.verified || false,
a.type || 'presence', a.justification || null, a.justificationAccepted ?? null
]);
}
logCount('frequencias', attendance.length);
}
async function migrateDisciplinas(client: pg.PoolClient, subjects: any[]) {
for (const s of subjects) {
await client.query(`
INSERT INTO disciplinas (id, nome, turma_id)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome
`, [s.id, s.name, s.classId || null]);
}
logCount('disciplinas', subjects.length);
}
async function migratePeriodos(client: pg.PoolClient, periods: any[]) {
for (const p of periods) {
await client.query(`
INSERT INTO periodos (id, nome)
VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome
`, [p.id, p.name]);
}
logCount('periodos', periods.length);
}
async function migrateNotas(client: pg.PoolClient, grades: any[]) {
for (const g of grades) {
await client.query(`
INSERT INTO notas (id, aluno_id, disciplina_id, valor, periodo)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET valor = EXCLUDED.valor
`, [g.id, g.studentId, g.subjectId, g.value || 0, g.period]);
}
logCount('notas', grades.length);
}
async function migratePagamentos(client: pg.PoolClient, payments: any[]) {
for (const p of payments) {
await client.query(`
INSERT INTO pagamentos (
id, aluno_id, contrato_id, valor, desconto, tipo_desconto, multa, juros,
vencimento, status, data_pagamento, tipo, numero_parcela, total_parcelas,
descricao, asaas_payment_id, asaas_payment_url, installment_id
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, data_pagamento = EXCLUDED.data_pagamento
`, [
p.id, p.studentId, p.contractId || null, p.amount, p.discount || 0,
p.discountType || null, p.lateFee || 0, p.interest || 0,
p.dueDate, p.status || 'pending', p.paidDate || null,
p.type || 'monthly', p.installmentNumber || null, p.totalInstallments || null,
p.description || null, p.asaasPaymentId || null, p.asaasPaymentUrl || null,
p.installmentId || null
]);
}
logCount('pagamentos', payments.length);
}
async function migrateContratos(client: pg.PoolClient, contracts: any[]) {
for (const c of 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 || new Date().toISOString()]);
}
logCount('contratos', contracts.length);
}
async function migrateModelosContrato(client: pg.PoolClient, templates: any[]) {
for (const t of templates) {
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]);
}
logCount('modelos_contrato', templates.length);
}
async function migrateNotificacoes(client: pg.PoolClient, notifications: any[]) {
for (const n of notifications) {
await client.query(`
INSERT INTO notificacoes (id, aluno_id, titulo, mensagem, lida, anexo, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET lida = EXCLUDED.lida
`, [n.id, n.studentId, n.title, n.message, n.read || false, n.attachment || null, n.createdAt || new Date().toISOString()]);
}
logCount('notificacoes', notifications.length);
}
async function migrateCertificados(client: pg.PoolClient, certificates: any[]) {
for (const c of certificates) {
await client.query(`
INSERT INTO certificados (id, aluno_id, descricao, imagem_frente, imagem_verso, data_emissao, overlays_frente, overlays_verso)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO NOTHING
`, [
c.id, c.studentId, c.description || null, c.frontImage, c.backImage || null,
c.issueDate, JSON.stringify(c.frontOverlays || []), JSON.stringify(c.backOverlays || [])
]);
}
logCount('certificados', certificates.length);
}
async function migrateApostilas(client: pg.PoolClient, handouts: any[]) {
for (const h of handouts) {
await client.query(`
INSERT INTO apostilas (id, nome, preco, descricao, multa_percentual, juros_percentual)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, preco = EXCLUDED.preco
`, [h.id, h.name, h.price || 0, h.description || null, h.finePercentage || 0, h.interestPercentage || 0]);
}
logCount('apostilas', handouts.length);
}
async function migrateEntregasApostilas(client: pg.PoolClient, deliveries: any[]) {
for (const d of deliveries) {
await client.query(`
INSERT INTO entregas_apostilas (id, aluno_id, apostila_id, status_entrega, status_pagamento, data_entrega, data_pagamento, asaas_payment_id, asaas_payment_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO NOTHING
`, [
d.id, d.studentId, d.handoutId, d.deliveryStatus || 'pending',
d.paymentStatus || 'pending', d.deliveryDate || null, d.paymentDate || null,
d.asaasPaymentId || null, d.asaasPaymentUrl || null
]);
}
logCount('entregas_apostilas', deliveries.length);
}
async function migrateFuncionarios(client: pg.PoolClient, categories: any[], employees: any[]) {
for (const c of categories) {
await client.query(`
INSERT INTO categorias_funcionarios (id, nome)
VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome
`, [c.id, c.name]);
}
logCount('categorias_funcionarios', categories.length);
for (const e of employees) {
await client.query(`
INSERT INTO funcionarios (id, nome, cpf, telefone, email, data_admissao, categoria_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome
`, [e.id, e.name, e.cpf || '', e.phone || '', e.email || '', e.admissionDate || null, e.categoryId || null]);
}
logCount('funcionarios', employees.length);
}
async function migrateProvas(client: pg.PoolClient, exams: any[]) {
for (const e of exams) {
await client.query(`
INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET titulo = EXCLUDED.titulo, status = EXCLUDED.status
`, [e.id, e.classId, e.subjectId || null, e.periodId || null, e.title, e.durationMinutes || 60, e.status || 'draft']);
// Migrar questões da prova
const questions = e.questions || [];
for (let i = 0; i < questions.length; i++) {
const q = questions[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)
ON CONFLICT (id) DO UPDATE SET texto = EXCLUDED.texto, opcoes = EXCLUDED.opcoes
`, [q.id, e.id, q.text, q.imageUrl || null, JSON.stringify(q.options || []), q.correctOptionIndex || 0, i]);
}
}
logCount('provas + questoes', exams.length);
}
// ============================================================
// MIGRAR TABELAS SEPARADAS DO SUPABASE
// ============================================================
async function migrateCobrancasAsaas(client: pg.PoolClient) {
log('🔄', 'Buscando tabela alunos_cobrancas do Supabase...');
const { data, error } = await supabase
.from('alunos_cobrancas')
.select('*');
if (error) {
log('⚠️', `Erro ao buscar alunos_cobrancas: ${error.message}. Pulando...`);
return;
}
const cobrancas = data || [];
for (const c of cobrancas) {
await client.query(`
INSERT INTO alunos_cobrancas (id, aluno_id, asaas_customer_id, asaas_payment_id, asaas_installment_id, installment, valor, vencimento, status, data_pagamento, link_boleto, link_carne, transaction_receipt_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (id) DO NOTHING
`, [
c.id, c.aluno_id, c.asaas_customer_id || null, c.asaas_payment_id || null,
c.asaas_installment_id || null, c.installment || null,
c.valor, c.vencimento, c.status || 'PENDENTE', c.data_pagamento || null,
c.link_boleto || null, c.link_carne || null, c.transaction_receipt_url || null
]);
}
logCount('alunos_cobrancas', cobrancas.length);
}
async function migrateSubmissoesProvas(client: pg.PoolClient) {
log('🔄', 'Buscando tabela provas_submissoes do Supabase...');
const { data, error } = await supabase
.from('provas_submissoes')
.select('*');
if (error) {
log('⚠️', `Erro ao buscar provas_submissoes: ${error.message}. Pulando...`);
return;
}
const subs = data || [];
for (const s of subs) {
await client.query(`
INSERT INTO provas_submissoes (id, aluno_id, prova_id, total_questoes, acertos, erros, percentual, nota_final, respostas, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (id) DO NOTHING
`, [
s.id || `sub-${Date.now()}`, s.aluno_id, s.exam_id,
s.total_questions || 0, s.correct_count || 0, s.wrong_count || 0,
s.percentage || 0, s.final_score || 0,
JSON.stringify(s.answers_json || {}), s.created_at || new Date().toISOString()
]);
}
logCount('provas_submissoes', subs.length);
}
// ============================================================
// BACKUP DO JSON COMPLETO (Segurança)
// ============================================================
async function saveJsonBackup(schoolData: any) {
const fileName = `backup_supabase_${new Date().toISOString().split('T')[0]}.json`;
fs.writeFileSync(fileName, JSON.stringify(schoolData, null, 2), 'utf8');
log('💾', `Backup completo salvo em: ${fileName}`);
}
// ============================================================
// FUNÇÃO PRINCIPAL
// ============================================================
async function main() {
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ MIGRAÇÃO EDUMANAGER: SUPABASE → POSTGRESQL LOCAL ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log('');
// 1. Buscar o JSON blob do Supabase
log('🌐', 'Conectando ao Supabase Cloud...');
const { data: schoolRow, error: fetchError } = await supabase
.from('school_data')
.select('data')
.eq('id', 1)
.single();
if (fetchError || !schoolRow?.data) {
log('❌', `FALHA AO CONECTAR AO SUPABASE: ${fetchError?.message || 'Dados não encontrados'}`);
process.exit(1);
}
const schoolData = schoolRow.data;
log('✅', 'Dados baixados do Supabase com sucesso!');
// 2. Salvar backup local primeiro (segurança)
await saveJsonBackup(schoolData);
// 3. Conectar ao PostgreSQL local
log('🔌', 'Conectando ao PostgreSQL local...');
const client = await pool.connect();
try {
// TRANSAÇÃO ATÔMICA: Tudo ou nada
await client.query('BEGIN');
log('🔒', 'Transação iniciada (modo atômico)');
// 4. Também salvar o JSON completo na tabela legada para ponte
log('📋', 'Salvando JSON blob na tabela school_data (ponte)...');
await client.query(`
INSERT INTO school_data (id, data, updated_at)
VALUES (1, $1, NOW())
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()
`, [JSON.stringify(schoolData)]);
// 5. Migrar entidade por entidade
console.log('');
log('🚀', '═══ INICIANDO MIGRAÇÃO TABELA POR TABELA ═══');
console.log('');
await migrateConfiguracoes(client, schoolData);
await migrateUsuarios(client, schoolData.users || []);
await migrateCursos(client, schoolData.courses || []);
await migrateTurmas(client, schoolData.classes || []);
await migrateAlunos(client, schoolData.students || []);
await migrateAulas(client, schoolData.lessons || []);
await migrateFrequencias(client, schoolData.attendance || []);
await migrateDisciplinas(client, schoolData.subjects || []);
await migratePeriodos(client, schoolData.periods || []);
await migrateNotas(client, schoolData.grades || []);
await migratePagamentos(client, schoolData.payments || []);
await migrateContratos(client, schoolData.contracts || []);
await migrateModelosContrato(client, schoolData.contractTemplates || []);
await migrateNotificacoes(client, schoolData.notifications || []);
await migrateCertificados(client, schoolData.certificates || []);
await migrateApostilas(client, schoolData.handouts || []);
await migrateEntregasApostilas(client, schoolData.handoutDeliveries || []);
await migrateFuncionarios(client, schoolData.employeeCategories || [], schoolData.employees || []);
await migrateProvas(client, schoolData.exams || []);
// 6. Migrar tabelas separadas do Supabase
console.log('');
log('🔄', '═══ MIGRANDO TABELAS SEPARADAS ═══');
console.log('');
await migrateCobrancasAsaas(client);
await migrateSubmissoesProvas(client);
// 7. Commit da transação
await client.query('COMMIT');
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ ✅ MIGRAÇÃO CONCLUÍDA COM SUCESSO! ║');
console.log('║ ║');
console.log('║ • Todos os dados foram copiados com integridade ║');
console.log('║ • Senhas mantidas EXATAMENTE como estavam ║');
console.log('║ • Backup JSON salvo localmente ║');
console.log('║ • Tabela school_data (legada) populada como ponte ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log('');
} catch (error: any) {
// ROLLBACK: Se qualquer coisa falhar, NADA é salvo
await client.query('ROLLBACK');
console.log('');
log('❌', '══════════════════════════════════════════════════');
log('❌', `ERRO NA MIGRAÇÃO: ${error.message}`);
log('❌', 'ROLLBACK executado. Nenhum dado foi alterado no PostgreSQL.');
log('❌', '══════════════════════════════════════════════════');
console.log('');
console.error(error);
} finally {
client.release();
await pool.end();
}
}
// Execução
main().catch(console.error);