/** * Script: create-demo-ventas * * Crea una cuenta demo completa para ventas: * - Tenant "Demo Ventas SA de CV" (plan custom, sin cobro) * - Usuario owner: demo@horuxfin.com / Demo12345! * - Base de datos propia con datos ficticios de contabilidad * - Contribuyente, clientes/proveedores, CFDIs, bancos, conciliaciones, * obligaciones fiscales y cartera. * * Ejecución: * cd apps/api && npx tsx scripts/create-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', nombre: 'Demo Ventas SA de CV', email: 'demo@horuxfin.com', password: 'Demo12345!', databaseName: 'horux_demoventas', codigoPostal: '01000', }; const CLIENTES = [ { rfc: 'CLI123456AB1', nombre: 'Cliente Alfa SA' }, { rfc: 'CLI123456AB2', nombre: 'Cliente Beta SA' }, { rfc: 'CLI123456AB3', nombre: 'Cliente Gamma SA' }, { rfc: 'CLI123456AB4', nombre: 'Cliente Delta SA' }, { rfc: 'CLI123456AB5', nombre: 'Cliente Epsilon SA' }, ]; const PROVEEDORES = [ { rfc: 'PRO123456AB1', nombre: 'Proveedor Materiales SA' }, { rfc: 'PRO123456AB2', nombre: 'Proveedor Servicios SA' }, { rfc: 'PRO123456AB3', nombre: 'Proveedor Logistica SA' }, { rfc: 'PRO123456AB4', nombre: 'Proveedor Tecnologia SA' }, { rfc: 'PRO123456AB5', nombre: 'Proveedor Papeleria 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' }, { clave: '50151500', descripcion: 'Materiales de oficina', unidad: 'Pieza' }, { clave: '80181600', descripcion: 'Publicidad', unidad: 'Servicio' }, { clave: '81112200', descripcion: 'Diseno grafico', unidad: 'Servicio' }, ]; function parseDatabaseUrl(url: string) { const parsed = new URL(url); return { host: parsed.hostname, port: parseInt(parsed.port || '5432'), user: decodeURIComponent(parsed.username), password: decodeURIComponent(parsed.password), }; } async function main() { console.log('🌱 Creando cuenta demo "Demo Ventas"...\n'); const ownerRole = await prisma.rol.findUnique({ where: { nombre: 'owner' } }); if (!ownerRole) throw new Error('Rol owner no encontrado en BD central'); // ============================================================ // 1. Tenant // ============================================================ let tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO.rfc } }); if (!tenant) { tenant = await prisma.tenant.create({ data: { nombre: DEMO.nombre, rfc: DEMO.rfc, plan: 'custom', databaseName: DEMO.databaseName, verticalProfile: 'CONTABLE', dbMode: 'MANAGED', dbSchemaVersion: 0, codigoPostal: DEMO.codigoPostal, active: true, }, }); console.log('✅ Tenant creado:', tenant.nombre, `(${tenant.rfc})`); } else { await prisma.tenant.update({ where: { id: tenant.id }, data: { plan: 'custom', active: true, verticalProfile: 'CONTABLE' }, }); console.log('✅ Tenant actualizado:', tenant.nombre, `(${tenant.rfc})`); } // ============================================================ // 2. Usuario owner // ============================================================ let user = await prisma.user.findUnique({ where: { email: DEMO.email } }); const passwordHash = await bcrypt.hash(DEMO.password, 12); if (!user) { user = await prisma.user.create({ data: { email: DEMO.email, passwordHash, nombre: 'Usuario Demo', lastTenantId: tenant.id, }, }); console.log('✅ Usuario creado:', user.email); } else { user = await prisma.user.update({ where: { id: user.id }, data: { passwordHash, lastTenantId: tenant.id }, }); console.log('✅ Usuario actualizado:', user.email); } // ============================================================ // 3. Membership // ============================================================ await prisma.tenantMembership.upsert({ where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } }, update: { rolId: ownerRole.id, isOwner: true, active: true }, create: { userId: user.id, tenantId: tenant.id, rolId: ownerRole.id, isOwner: true, active: true, }, }); console.log('✅ Membership owner asignada'); // ============================================================ // 4. Suscripción custom gratis/ilimitada (status authorized) // ============================================================ const now = new Date(); const periodEnd = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); const existingSub = await prisma.subscription.findFirst({ where: { tenantId: tenant.id }, orderBy: { createdAt: 'desc' }, }); if (!existingSub) { await prisma.subscription.create({ data: { tenantId: tenant.id, plan: 'custom', status: 'authorized', amount: 0, frequency: 'monthly', currentPeriodStart: now, currentPeriodEnd: periodEnd, }, }); } else { await prisma.subscription.update({ where: { id: existingSub.id }, data: { plan: 'custom', status: 'authorized', amount: 0, currentPeriodStart: now, currentPeriodEnd: periodEnd, pendingPlan: null, pendingFrequency: null, pendingEffectiveAt: null, upgradePreferenceId: null, upgradeTargetPlan: null, upgradeTargetAmount: null, }, }); } console.log('✅ Suscripción custom activa (gratis)'); // ============================================================ // 5. Régimen fiscal activo del tenant // ============================================================ const regimen = await prisma.regimen.findUnique({ where: { clave: '601' } }); if (regimen) { await prisma.tenantRegimenActivo.upsert({ where: { tenantId_regimenId: { tenantId: tenant.id, regimenId: regimen.id } }, update: {}, create: { tenantId: tenant.id, regimenId: regimen.id }, }); console.log('✅ Régimen 601 activado para el tenant'); } // ============================================================ // 6. Base de datos del tenant // ============================================================ await tenantDb.provisionDatabase(DEMO.rfc, DEMO.databaseName); const pool = await tenantDb.getPool(tenant.id, DEMO.databaseName); console.log('✅ Base de datos del tenant provisionada:', DEMO.databaseName); // ============================================================ // 7. Datos ficticios en BD del tenant // ============================================================ await seedTenantData(pool, tenant.id, user.id); console.log('\n🎉 Demo Ventas lista'); console.log(' Login:', DEMO.email, '/', DEMO.password); console.log(' Tenant:', DEMO.nombre, `(${DEMO.rfc})`); console.log(' BD:', DEMO.databaseName); } async function seedTenantData(pool: Pool, tenantId: string, ownerId: string) { const client = await pool.connect(); try { await client.query('BEGIN'); // Contribuyente principal const { rows: [entidad] } = await client.query<{ id: string }>(` INSERT INTO entidades_gestionadas (tipo, nombre, identificador, supervisor_user_id) VALUES ('CONTRIBUYENTE', $1, $2, $3) ON CONFLICT DO NOTHING RETURNING id `, [DEMO.nombre, DEMO.rfc, ownerId]); let contribuyenteId: string; if (entidad) { contribuyenteId = entidad.id; await client.query(` INSERT INTO contribuyentes (entidad_id, rfc, regimen_fiscal, codigo_postal) VALUES ($1, $2, $3, $4) ON CONFLICT (entidad_id) DO NOTHING `, [contribuyenteId, DEMO.rfc, '601', DEMO.codigoPostal]); } else { const { rows: [existing] } = await client.query<{ id: string }>(` SELECT e.id FROM entidades_gestionadas e JOIN contribuyentes c ON c.entidad_id = e.id WHERE e.identificador = $1 `, [DEMO.rfc]); contribuyenteId = existing.id; } console.log('✅ Contribuyente principal creado:', DEMO.rfc); // RFCs de clientes y 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, c.rfc.startsWith('CLI') ? '601' : '601']); rfcs.set(c.rfc, r.id); } // RFC del contribuyente principal 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 `, [DEMO.rfc, DEMO.nombre, '601']); rfcs.set(DEMO.rfc, rfcPrincipal.id); // Bancos del contribuyente const { rows: [banco1] } = await client.query<{ id: number }>(` INSERT INTO bancos (banco, terminacion_cuenta, contribuyente_id) VALUES ($1, $2, $3) RETURNING id `, ['BBVA', '1234', contribuyenteId]); const { rows: [banco2] } = await client.query<{ id: number }>(` INSERT INTO bancos (banco, terminacion_cuenta, contribuyente_id) VALUES ($1, $2, $3) RETURNING id `, ['Santander', '5678', contribuyenteId]); console.log('✅ Bancos creados'); // Generar CFDIs const tipos: Array<'EMITIDO' | 'RECIBIDO'> = ['EMITIDO', 'RECIBIDO']; const cfdiIds: number[] = []; for (let i = 0; i < 60; i++) { const tipo = tipos[i % 2]; const esEmitido = tipo === 'EMITIDO'; const contraparte = esEmitido ? CLIENTES[i % CLIENTES.length] : PROVEEDORES[i % PROVEEDORES.length]; const subtotal = Math.floor(Math.random() * 40000) + 2000; const iva = Math.round(subtotal * 0.16 * 100) / 100; const total = Math.round((subtotal + iva) * 100) / 100; const daysAgo = Math.floor(Math.random() * 540); // hasta ~18 meses atrás const fecha = new Date(); fecha.setDate(fecha.getDate() - daysAgo); fecha.setHours(10 + (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.3 ? 'PUE' : 'PPD'; const formasPago = ['01', '02', '03', '04']; const formaPago = formasPago[i % formasPago.length]; const usoCfdi = esEmitido ? 'G03' : 'G01'; const rfcEmisor = esEmitido ? DEMO.rfc : contraparte.rfc; const nombreEmisor = esEmitido ? DEMO.nombre : contraparte.nombre; const rfcReceptor = esEmitido ? contraparte.rfc : DEMO.rfc; const nombreReceptor = esEmitido ? contraparte.nombre : DEMO.nombre; 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(1000 + 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, ]); cfdiIds.push(cfdi.id); // Conceptos const numConceptos = Math.floor(Math.random() * 3) + 1; for (let j = 0; j < numConceptos; j++) { const prod = PRODUCTOS[(i + j) % PRODUCTOS.length]; const cantidad = Math.floor(Math.random() * 5) + 1; const valorUnitario = Math.floor(Math.random() * 4000) + 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, ]); } } console.log('✅ 60 CFDIs y conceptos creados'); // Conciliaciones para algunos CFDIs PPD pagados con transferencia (forma 02/03) const { rows: cfdisPpd } = await client.query<{ id: number; year: string; month: string }>(` SELECT id, year, month FROM cfdis WHERE metodo_pago = 'PPD' AND forma_pago IN ('02', '03') ORDER BY id LIMIT 15 `); for (const c of cfdisPpd) { const bancoId = Math.random() > 0.5 ? banco1.id : banco2.id; const fechaPago = new Date(); fechaPago.setDate(fechaPago.getDate() - Math.floor(Math.random() * 30)); await client.query(` INSERT INTO conciliaciones (anio, mes, id_cfdi, fecha_de_pago, id_banco) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id_cfdi) DO NOTHING `, [c.year, c.month, c.id, fechaPago.toISOString().split('T')[0], bancoId]); } console.log('✅ Conciliaciones creadas'); // Obligaciones fiscales asignadas al contribuyente const obligaciones = [ { id: 'isr-provisional', nombre: 'Pago provisional de ISR', categoria: 'Federal mensual' }, { id: 'iva-mensual', nombre: 'Pago mensual definitivo de IVA', categoria: 'Federal mensual' }, { id: 'ret-isr-honorarios', nombre: 'Retenciones de ISR por honorarios y arrendamiento a PF', categoria: 'Federal mensual' }, { id: 'diot', nombre: 'DIOT', categoria: 'Informativa mensual' }, { id: 'imss-cuotas', nombre: 'Cuotas obrero-patronales IMSS', categoria: 'Seguridad social' }, { id: 'anual-isr-pm', nombre: 'Declaración Anual de ISR PM', categoria: 'Anual' }, { id: 'isn', nombre: 'ISN - Impuesto Sobre Nómina', categoria: 'Estatal' }, { id: 'isrtp', nombre: 'Impuesto sobre remuneración al trabajo', categoria: 'Estatal' }, ]; for (const o of obligaciones) { await client.query(` INSERT INTO obligaciones_contribuyente ( contribuyente_id, catalogo_id, nombre, frecuencia, fecha_limite, categoria, activa, es_recomendada ) VALUES ($1, $2, $3, $4, $5, $6, true, true) ON CONFLICT DO NOTHING `, [contribuyenteId, o.id, o.nombre, 'mensual', 'Día 17 del mes siguiente', o.categoria]); } console.log('✅ Obligaciones fiscales asignadas'); // Cartera principal con el contribuyente const { rows: [cartera] } = await client.query<{ id: string }>(` INSERT INTO carteras (supervisor_user_id, nombre, descripcion) VALUES ($1, $2, $3) RETURNING id `, [ownerId, 'Cartera Principal', 'Clientes y prospectos de Demo Ventas']); await client.query(` INSERT INTO cartera_entidades (cartera_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING `, [cartera.id, contribuyenteId]); console.log('✅ Cartera principal creada'); // Alertas y recordatorios de ejemplo await client.query(` INSERT INTO alertas (tipo, titulo, mensaje, prioridad, fecha_vencimiento) VALUES ('obligacion', 'Declaración mensual de IVA', 'Pago de IVA correspondiente a mayo 2026', 'alta', NOW() + INTERVAL '10 days'), ('obligacion', 'Pago provisional ISR', 'Pago provisional de ISR de mayo 2026', 'alta', NOW() + INTERVAL '10 days'), ('sat', 'Sincronización SAT pendiente', 'Última sincronización hace más de 7 días', 'media', NOW() + INTERVAL '3 days') `); await client.query(` INSERT INTO recordatorios (titulo, descripcion, fecha_limite, notas, completado, privado, creado_por) VALUES ('Revisar estados de cuenta', 'Conciliar pagos de clientes', NOW() + INTERVAL '5 days', 'Prioridad alta', false, false, $1), ('Enviar facturas del mes', 'Facturación recurrente a clientes', NOW() + INTERVAL '7 days', 'Clientes Alfa y Beta', false, false, $1) `, [ownerId]); console.log('✅ Alertas y recordatorios de ejemplo creados'); await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } main() .catch((e) => { console.error('\n❌ Error creando demo:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); await tenantDb.shutdown(); });