- 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
302 lines
8.6 KiB
TypeScript
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();
|
|
});
|