/** * ETL: Migración de BD central Horux360 → HoruxDespachos * Lee desde horux360 y escribe en la BD central actual (horux_despachos) */ import 'dotenv/config'; import { PrismaClient } from '@prisma/client'; import { Pool } from 'pg'; const SOURCE_DB = 'horux360'; const prisma = new PrismaClient(); const source = new Pool({ host: 'localhost', port: 5432, user: 'postgres', password: 'ZxHMrmnwanvLfLDdNJdRthFjWF2Lj1Rb', database: SOURCE_DB, max: 5, }); async function main() { console.log('=== Central DB Migration: Horux360 → HoruxDespachos ===\n'); // 1. Seed roles (idempotente) const rolesData = [ { nombre: 'owner', descripcion: 'Dueño - acceso completo' }, { nombre: 'cfo', descripcion: 'CFO - acceso completo (mismo nivel que el dueño)' }, { nombre: 'contador', descripcion: 'Contador - dashboard, CFDI, impuestos, calendario, alertas, facturación' }, { nombre: 'auxiliar', descripcion: 'Auxiliar - mismos permisos que contador' }, { nombre: 'visor', descripcion: 'Visor - solo lectura de CFDI, impuestos, calendario, alertas' }, { nombre: 'supervisor', descripcion: 'Supervisor de despacho — titular de RFCs, crea carteras' }, { nombre: 'cliente', descripcion: 'Cliente visor externo — acceso read-only a sus RFCs' }, ]; for (const r of rolesData) { await prisma.rol.upsert({ where: { nombre: r.nombre }, update: {}, create: r, }); } console.log('✅ Roles seeded'); const roles = await prisma.rol.findMany(); const rolMap = new Map(roles.map(r => [r.nombre, r.id])); // 2. Migrate tenants const tenantsLegacy = await source.query(` SELECT id, nombre, rfc, plan, database_name, cfdi_limit, users_limit, active, created_at, expires_at FROM tenants `); const planMap: Record = { starter: 'starter', business: 'business', professional: 'business_ia', enterprise: 'enterprise', }; for (const t of tenantsLegacy.rows) { await prisma.tenant.upsert({ where: { rfc: t.rfc }, update: {}, create: { id: t.id, nombre: t.nombre, rfc: t.rfc, plan: (planMap[t.plan] || 'starter') as any, databaseName: t.database_name, cfdiLimit: t.cfdi_limit, usersLimit: t.users_limit, active: t.active, createdAt: t.created_at, expiresAt: t.expires_at, }, }); } console.log(`✅ ${tenantsLegacy.rows.length} tenants migrated`); // 3. Migrate users (sin tenantId/rolId legacy) const usersLegacy = await source.query(` SELECT id, email, password_hash, nombre, active, last_login, created_at FROM users `); for (const u of usersLegacy.rows) { await prisma.user.upsert({ where: { email: u.email }, update: {}, create: { id: u.id, email: u.email, passwordHash: u.password_hash, nombre: u.nombre, active: u.active, lastLogin: u.last_login, createdAt: u.created_at, tokenVersion: 0, }, }); } console.log(`✅ ${usersLegacy.rows.length} users migrated`); // 4. Create TenantMemberships (backfill from legacy user→tenant relationship) const membershipsLegacy = await source.query(` SELECT u.id as user_id, u.tenant_id, u.role, u.active, u.created_at FROM users u `); const roleNameMap: Record = { admin: 'owner', contador: 'contador', visor: 'visor', }; for (const m of membershipsLegacy.rows) { const roleName = roleNameMap[m.role] || 'visor'; const rolId = rolMap.get(roleName); if (!rolId) continue; await prisma.tenantMembership.upsert({ where: { userId_tenantId: { userId: m.user_id, tenantId: m.tenant_id, }, }, update: {}, create: { userId: m.user_id, tenantId: m.tenant_id, rolId, isOwner: roleName === 'owner' || roleName === 'cfo', active: m.active, joinedAt: m.created_at, }, }); } console.log(`✅ ${membershipsLegacy.rows.length} tenant memberships created`); // 5. Set lastTenantId for all users for (const m of membershipsLegacy.rows) { await prisma.user.update({ where: { id: m.user_id }, data: { lastTenantId: m.tenant_id }, }); } console.log('✅ lastTenantId set for all users'); // 6. Migrate FIEL credentials const fielLegacy = await source.query(`SELECT * FROM fiel_credentials`); for (const f of fielLegacy.rows) { await prisma.fielCredential.upsert({ where: { tenantId: f.tenant_id }, update: {}, create: { id: f.id, tenantId: f.tenant_id, rfc: f.rfc, cerData: f.cer_data, keyData: f.key_data, keyPasswordEncrypted: f.key_password_encrypted, cerIv: f.cer_iv, cerTag: f.cer_tag, keyIv: f.key_iv, keyTag: f.key_tag, passwordIv: f.password_iv, passwordTag: f.password_tag, serialNumber: f.serial_number, validFrom: f.valid_from, validUntil: f.valid_until, isActive: f.is_active, createdAt: f.created_at, updatedAt: f.updated_at, }, }); } console.log(`✅ ${fielLegacy.rows.length} FIEL credentials migrated`); // 7. Migrate subscriptions const subsLegacy = await source.query(`SELECT * FROM subscriptions`); for (const s of subsLegacy.rows) { await prisma.subscription.create({ data: { id: s.id, tenantId: s.tenant_id, plan: (planMap[s.plan] || 'starter') as any, mpPreapprovalId: s.mp_preapproval_id, status: s.status, amount: s.amount, frequency: s.frequency, currentPeriodStart: s.current_period_start, currentPeriodEnd: s.current_period_end, createdAt: s.created_at, updatedAt: s.updated_at, }, }); } console.log(`✅ ${subsLegacy.rows.length} subscriptions migrated`); // 8. Migrate payments const paymentsLegacy = await source.query(`SELECT * FROM payments`); for (const p of paymentsLegacy.rows) { await prisma.payment.create({ data: { id: p.id, tenantId: p.tenant_id, subscriptionId: p.subscription_id, mpPaymentId: p.mp_payment_id, amount: p.amount, status: p.status, paymentMethod: p.payment_method, paidAt: p.paid_at, createdAt: p.created_at, }, }); } console.log(`✅ ${paymentsLegacy.rows.length} payments migrated`); // 9. Migrate SAT sync jobs const jobsLegacy = await source.query(`SELECT * FROM sat_sync_jobs`); for (const j of jobsLegacy.rows) { await prisma.satSyncJob.create({ data: { id: j.id, tenantId: j.tenant_id, type: j.type, status: j.status, dateFrom: j.date_from, dateTo: j.date_to, cfdiType: j.cfdi_type, satRequestId: j.sat_request_id, satPackageIds: j.sat_package_ids || [], cfdisFound: j.cfdis_found, cfdisDownloaded: j.cfdis_downloaded, cfdisInserted: j.cfdis_inserted, cfdisUpdated: j.cfdis_updated, progressPercent: j.progress_percent, errorMessage: j.error_message, startedAt: j.started_at, completedAt: j.completed_at, createdAt: j.created_at, retryCount: j.retry_count, nextRetryAt: j.next_retry_at, }, }); } console.log(`✅ ${jobsLegacy.rows.length} SAT sync jobs migrated`); // 10. Migrate refresh tokens const tokensLegacy = await source.query(`SELECT * FROM refresh_tokens`); for (const t of tokensLegacy.rows) { await prisma.refreshToken.create({ data: { id: t.id, userId: t.user_id, token: t.token, expiresAt: t.expires_at, createdAt: t.created_at, }, }); } console.log(`✅ ${tokensLegacy.rows.length} refresh tokens migrated`); // 11. Create platform_admin for global admin tenant (CAS2408138W2) const globalAdmins = await prisma.tenantMembership.findMany({ where: { tenant: { rfc: 'CAS2408138W2' }, isOwner: true, }, }); for (const m of globalAdmins) { await prisma.userPlatformRole.upsert({ where: { userId_role: { userId: m.userId, role: 'platform_admin', }, }, update: {}, create: { userId: m.userId, role: 'platform_admin', }, }); } console.log(`✅ ${globalAdmins.length} platform_admin roles assigned`); console.log('\n=== Central migration complete ==='); } main() .catch((err) => { console.error('Migration failed:', err); process.exit(1); }) .finally(async () => { await source.end(); await prisma.$disconnect(); });