Files
HoruxDespachos/apps/api/scripts/migrate-central-legacy.ts
Horux Dev e8dc3aed67 feat: metabase auto-registration + ui fixes + migration scripts
- Add metabase.service.ts for automatic DB registration on tenant creation
- Hook createTenant, addTenantToOwner and deleteTenant to sync with Metabase
- Add environment variables for Metabase integration
- Fix dashboard routing for global admin users
- Fix CFDI status casing (Vigente vs vigente)
- Fix sidebar empty nav crash
- Fix KPI null regimen_fiscal values
- Fix CFDI type mapping (EMITIDO/RECIBIDO)
- Update branding from Horux360 to Horux Despachos
- Add legacy migration scripts for central and tenant DBs
2026-04-28 00:34:41 +00:00

302 lines
8.6 KiB
TypeScript

/**
* 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<string, string> = {
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<string, string> = {
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();
});