/** * Script: update-demo-ventas * * Agrega al tenant Demo Ventas: * - 5 contribuyentes adicionales * - Usuarios supervisor, auxiliar y cliente con sus memberships * - CFDIs de ejemplo para los nuevos contribuyentes * - Accesos de cliente a los contribuyentes * - Ajusta el plan custom para soportar más RFCs/usuarios * * Ejecución: * cd apps/api && npx tsx scripts/update-demo-ventas.ts */ import { PrismaClient } from '@prisma/client'; import { Pool } from 'pg'; import bcrypt from 'bcryptjs'; import { randomUUID } from 'crypto'; import { tenantDb } from '../src/config/database.ts'; const prisma = new PrismaClient(); const DEMO_RFC = 'DEMO2501019X2'; const DEFAULT_PASSWORD = 'Demo12345!'; const NUEVOS_CONTRIBUYENTES = [ { rfc: 'COM2501019X1', nombre: 'Comercial del Norte SA de CV', cp: '64000' }, { rfc: 'DIS2501019X1', nombre: 'Distribuidora del Centro SA de CV', cp: '44100' }, { rfc: 'SIS2501019X1', nombre: 'Servicios Integrales del Sur SA de CV', cp: '86000' }, { rfc: 'IMP2501019X1', nombre: 'Importadora del Pacifico SA de CV', cp: '82140' }, { rfc: 'EXA2501019X1', nombre: 'Exportadora del Atlantico SA de CV', cp: '94270' }, ]; const USUARIOS = [ { email: 'supervisor@horuxfin.com', nombre: 'Supervisor Demo', rol: 'supervisor' }, { email: 'auxiliar@horuxfin.com', nombre: 'Auxiliar Demo', rol: 'auxiliar' }, { email: 'cliente@horuxfin.com', nombre: 'Cliente Demo', rol: 'cliente' }, ]; const CLIENTES = [ { rfc: 'CLI123456AB1', nombre: 'Cliente Alfa SA' }, { rfc: 'CLI123456AB2', nombre: 'Cliente Beta SA' }, { rfc: 'CLI123456AB3', nombre: 'Cliente Gamma SA' }, ]; const PROVEEDORES = [ { rfc: 'PRO123456AB1', nombre: 'Proveedor Materiales SA' }, { rfc: 'PRO123456AB2', nombre: 'Proveedor Servicios SA' }, { rfc: 'PRO123456AB3', nombre: 'Proveedor Logistica SA' }, ]; const PRODUCTOS = [ { clave: '84111506', descripcion: 'Servicio de consultoria', unidad: 'Servicio' }, { clave: '43232408', descripcion: 'Licencia de software', unidad: 'Licencia' }, { clave: '81141500', descripcion: 'Soporte tecnico', unidad: 'Servicio' }, { clave: '81121700', descripcion: 'Desarrollo web', unidad: 'Servicio' }, { clave: '86101500', descripcion: 'Capacitacion', unidad: 'Servicio' }, ]; async function main() { console.log('🌱 Actualizando Demo Ventas...\n'); const tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO_RFC } }); if (!tenant) throw new Error(`Tenant ${DEMO_RFC} no encontrado`); // Ajustar catálogo del plan custom para soportar la demo completa await prisma.despachoPlanPrice.update({ where: { plan: 'custom' }, data: { maxRfcs: 10, maxUsers: 10 }, }); console.log('✅ Plan custom actualizado: maxRfcs=10, maxUsers=10'); // Crear/actualizar usuarios y memberships const createdUsers: Record = {}; for (const u of USUARIOS) { const rol = await prisma.rol.findUnique({ where: { nombre: u.rol } }); if (!rol) throw new Error(`Rol ${u.rol} no encontrado`); let user = await prisma.user.findUnique({ where: { email: u.email } }); const passwordHash = await bcrypt.hash(DEFAULT_PASSWORD, 12); if (!user) { user = await prisma.user.create({ data: { email: u.email, passwordHash, nombre: u.nombre, lastTenantId: tenant.id }, }); } else { user = await prisma.user.update({ where: { id: user.id }, data: { passwordHash, lastTenantId: tenant.id } }); } await prisma.tenantMembership.upsert({ where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } }, update: { rolId: rol.id, active: true, isOwner: false }, create: { userId: user.id, tenantId: tenant.id, rolId: rol.id, active: true, isOwner: false }, }); createdUsers[u.rol] = { id: user.id, rolId: rol.id }; console.log(`✅ Usuario ${u.rol}:`, u.email); } const supervisorId = createdUsers.supervisor.id; const clienteId = createdUsers.cliente.id; // Conectar a BD del tenant const pool = await tenantDb.getPool(tenant.id, tenant.databaseName); // Crear contribuyentes, CFDIs y accesos const contribuyenteIds: string[] = []; for (const c of NUEVOS_CONTRIBUYENTES) { const id = await crearContribuyente(pool, c, supervisorId, tenant.id); contribuyenteIds.push(id); console.log(`✅ Contribuyente creado: ${c.rfc}`); await crearCfdis(pool, id, c.rfc, c.nombre); } // Asignar accesos de cliente a todos los contribuyentes (incluido el original) const { rows: todasEntidades } = await pool.query<{ id: string }>(` SELECT entidad_id AS id FROM contribuyentes `); for (const e of todasEntidades) { await pool.query(` INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING `, [clienteId, e.id]); } console.log('✅ Accesos de cliente asignados a', todasEntidades.length, 'contribuyentes'); // Agregar nuevos contribuyentes a la cartera principal const { rows: [cartera] } = await pool.query<{ id: string }>(` SELECT id FROM carteras ORDER BY created_at LIMIT 1 `); if (cartera) { for (const id of contribuyenteIds) { await pool.query(` INSERT INTO cartera_entidades (cartera_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING `, [cartera.id, id]); } console.log('✅ Nuevos contribuyentes agregados a cartera principal'); } console.log('\n🎉 Demo Ventas actualizada'); console.log(' Nuevos contribuyentes:', NUEVOS_CONTRIBUYENTES.length); console.log(' Usuos adicionales:'); for (const u of USUARIOS) { console.log(` ${u.rol}: ${u.email} / ${DEFAULT_PASSWORD}`); } } async function crearContribuyente(pool: Pool, data: { rfc: string; nombre: string; cp: string }, supervisorId: string, tenantId: string): Promise { const client = await pool.connect(); try { await client.query('BEGIN'); // Buscar si ya existe la entidad para este RFC const { rows: existingEntidad } = await client.query<{ id: string }>(` SELECT e.id FROM entidades_gestionadas e WHERE e.identificador = $1 AND e.tipo = 'CONTRIBUYENTE' `, [data.rfc]); let entidadId: string; if (existingEntidad.length > 0) { entidadId = existingEntidad[0].id; await client.query(` UPDATE entidades_gestionadas SET nombre = $1, supervisor_user_id = $2, updated_at = now() WHERE id = $3 `, [data.nombre, supervisorId, entidadId]); } else { const { rows: [entidad] } = await client.query<{ id: string }>(` INSERT INTO entidades_gestionadas (tipo, nombre, identificador, supervisor_user_id) VALUES ('CONTRIBUYENTE', $1, $2, $3) RETURNING id `, [data.nombre, data.rfc, supervisorId]); entidadId = entidad.id; } const { rows: existingContrib } = await client.query<{ entidad_id: string }>(` SELECT entidad_id FROM contribuyentes WHERE entidad_id = $1 `, [entidadId]); if (existingContrib.length > 0) { await client.query(` UPDATE contribuyentes SET rfc = $1, regimen_fiscal = $2, codigo_postal = $3 WHERE entidad_id = $4 `, [data.rfc, '601', data.cp, entidadId]); } else { await client.query(` INSERT INTO contribuyentes (entidad_id, rfc, regimen_fiscal, codigo_postal) VALUES ($1, $2, $3, $4) `, [entidadId, data.rfc, '601', data.cp]); } await client.query(` INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3) ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social `, [data.rfc, data.nombre, '601']); await client.query('COMMIT'); return entidadId; } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } async function crearCfdis(pool: Pool, contribuyenteId: string, rfcContribuyente: string, nombreContribuyente: string) { const client = await pool.connect(); try { await client.query('BEGIN'); // Asegurar RFCs de clientes/proveedores const rfcs = new Map(); for (const c of [...CLIENTES, ...PROVEEDORES]) { const { rows: [r] } = await client.query<{ id: number }>(` INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3) ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social RETURNING id `, [c.rfc, c.nombre, '601']); rfcs.set(c.rfc, r.id); } const { rows: [rfcPrincipal] } = await client.query<{ id: number }>(` INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3) ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social RETURNING id `, [rfcContribuyente, nombreContribuyente, '601']); rfcs.set(rfcContribuyente, rfcPrincipal.id); for (let i = 0; i < 10; i++) { const esEmitido = i < 5; const contraparte = esEmitido ? CLIENTES[i % CLIENTES.length] : PROVEEDORES[i % PROVEEDORES.length]; const subtotal = Math.floor(Math.random() * 30000) + 1500; const iva = Math.round(subtotal * 0.16 * 100) / 100; const total = Math.round((subtotal + iva) * 100) / 100; const daysAgo = Math.floor(Math.random() * 360); const fecha = new Date(); fecha.setDate(fecha.getDate() - daysAgo); fecha.setHours(9 + (i % 8), 0, 0, 0); const year = String(fecha.getFullYear()); const month = String(fecha.getMonth() + 1).padStart(2, '0'); const fechaStr = fecha.toISOString(); const metodoPago = Math.random() > 0.4 ? 'PUE' : 'PPD'; const formasPago = ['01', '02', '03']; const formaPago = formasPago[i % formasPago.length]; const usoCfdi = esEmitido ? 'G03' : 'G01'; const tipo = esEmitido ? 'EMITIDO' : 'RECIBIDO'; const rfcEmisor = esEmitido ? rfcContribuyente : contraparte.rfc; const nombreEmisor = esEmitido ? nombreContribuyente : contraparte.nombre; const rfcReceptor = esEmitido ? contraparte.rfc : rfcContribuyente; const nombreReceptor = esEmitido ? contraparte.nombre : nombreContribuyente; const { rows: [cfdi] } = await client.query<{ id: number }>(` INSERT INTO cfdis ( year, month, type, uuid, serie, folio, status, fecha_emision, rfc_emisor_id, rfc_emisor, nombre_emisor, rfc_receptor_id, rfc_receptor, nombre_receptor, subtotal, subtotal_mxn, descuento, descuento_mxn, total, total_mxn, moneda, tipo_cambio, tipo_comprobante, metodo_pago, forma_pago, uso_cfdi, iva_traslado, iva_traslado_mxn, regimen_fiscal_emisor, regimen_fiscal_receptor, contribuyente_id, fecha_efectiva, meses_global, año_global ) 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, $30, $31, $32, $33, $34 ) RETURNING id `, [ year, month, tipo, randomUUID(), 'DEMO', String(2000 + i), 'Vigente', fechaStr, rfcs.get(rfcEmisor), rfcEmisor, nombreEmisor, rfcs.get(rfcReceptor), rfcReceptor, nombreReceptor, subtotal, subtotal, 0, 0, total, total, 'MXN', 1, 'I', metodoPago, formaPago, usoCfdi, iva, iva, '601', '601', contribuyenteId, fechaStr, month, year, ]); const numConceptos = Math.floor(Math.random() * 2) + 1; for (let j = 0; j < numConceptos; j++) { const prod = PRODUCTOS[(i + j) % PRODUCTOS.length]; const cantidad = Math.floor(Math.random() * 4) + 1; const valorUnitario = Math.floor(Math.random() * 3000) + 500; const importe = Math.round(cantidad * valorUnitario * 100) / 100; const ivaConcepto = Math.round(importe * 0.16 * 100) / 100; await client.query(` INSERT INTO cfdi_conceptos ( cfdi_id, clave_prod_serv, descripcion, cantidad, clave_unidad, unidad, valor_unitario, valor_unitario_mxn, importe, importe_mxn, iva_traslado, iva_traslado_mxn ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) `, [ cfdi.id, prod.clave, prod.descripcion, cantidad, 'E48', prod.unidad, valorUnitario, valorUnitario, importe, importe, ivaConcepto, ivaConcepto, ]); } } await client.query('COMMIT'); console.log(` 📄 10 CFDIs creados para ${rfcContribuyente}`); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } main() .catch((e) => { console.error('\n❌ Error actualizando demo:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); await tenantDb.shutdown(); });