Files
HoruxDespachosNuevo/apps/api/prisma/seed.ts
Horux Dev 9f11a0ba39 feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos
Backend:
- Notificación email al admin cuando llega primer pago aprobado (sin factura auto)
- Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global
- Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d)
- Fix webhook MP: validación defensiva de x-signature header
- Fix autocompleto RFCs: eliminado filtro por contribuyenteId
- Fix autocompleto conceptos: eliminado filtro por contribuyenteId
- SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds
- SAT sync request reuse across jobs para evitar agotar cuota diaria
- Typo fix MP_ACCESS_TOKEN en .env
- Trial invitations system backend

Frontend:
- Nueva página /admin/facturas-pendientes con tabla y emisión manual
- Métrica 'Facturas pendientes' en /clientes (clickable)
- Navegación onboarding FIEL/CSD corregida
- Sidebar themes sincronizados
- Fix SAT portal migration scraper (NetIQ)
- Trial invitation acceptance pages
2026-05-09 21:56:42 +00:00

529 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { Pool } from 'pg';
import { migrate } from '../src/config/tenant-migrations.js';
import { RESICO_TASAS, ISR_TARIFAS } from './isr-data.js';
import { EVENTOS_FISCALES, DIAS_INHABILES } from './eventos-fiscales-data.js';
import {
FORMAS_PAGO, METODOS_PAGO, USOS_CFDI, MONEDAS, CLAVES_UNIDAD,
OBJETOS_IMP, TIPOS_RELACION, EXPORTACIONES,
} from './catalogos-sat-data.js';
const prisma = new PrismaClient();
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),
};
}
const REGIMENES_SAT = [
{ clave: '601', descripcion: 'General de Ley Personas Morales', tipoPersona: 'moral' },
{ clave: '603', descripcion: 'Personas Morales con Fines no Lucrativos', tipoPersona: 'moral' },
{ clave: '605', descripcion: 'Sueldos y Salarios e Ingresos Asimilados a Salarios', tipoPersona: 'fisica' },
{ clave: '606', descripcion: 'Arrendamiento', tipoPersona: 'fisica' },
{ clave: '607', descripcion: 'Régimen de Enajenación o Adquisición de Bienes', tipoPersona: 'fisica' },
{ clave: '608', descripcion: 'Demás ingresos', tipoPersona: 'fisica' },
{ clave: '610', descripcion: 'Residentes en el Extranjero sin Establecimiento Permanente en México', tipoPersona: 'ambos' },
{ clave: '611', descripcion: 'Ingresos por Dividendos (socios y accionistas)', tipoPersona: 'fisica' },
{ clave: '612', descripcion: 'Personas Físicas con Actividades Empresariales y Profesionales', tipoPersona: 'fisica' },
{ clave: '614', descripcion: 'Ingresos por intereses', tipoPersona: 'fisica' },
{ clave: '615', descripcion: 'Régimen de los ingresos por obtención de premios', tipoPersona: 'fisica' },
{ clave: '616', descripcion: 'Sin obligaciones fiscales', tipoPersona: 'ambos' },
{ clave: '620', descripcion: 'Sociedades Cooperativas de Producción que optan por diferir sus ingresos', tipoPersona: 'moral' },
{ clave: '621', descripcion: 'Incorporación Fiscal', tipoPersona: 'fisica' },
{ clave: '622', descripcion: 'Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras', tipoPersona: 'ambos' },
{ clave: '623', descripcion: 'Opcional para Grupos de Sociedades', tipoPersona: 'moral' },
{ clave: '624', descripcion: 'Coordinados', tipoPersona: 'moral' },
{ clave: '625', descripcion: 'Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', tipoPersona: 'fisica' },
{ clave: '626', descripcion: 'Régimen Simplificado de Confianza', tipoPersona: 'ambos' },
];
async function main() {
console.log('🌱 Seeding database...');
// Seed regimenes catalog
for (const r of REGIMENES_SAT) {
await prisma.regimen.upsert({
where: { clave: r.clave },
update: { descripcion: r.descripcion, tipoPersona: r.tipoPersona },
create: r,
});
}
console.log(`${REGIMENES_SAT.length} regímenes fiscales SAT cargados`);
// Seed ISR tables — limpiar y recrear
await prisma.isrResicoTasa.deleteMany();
await prisma.isrTarifa.deleteMany();
for (const anio of [2020, 2021, 2022, 2023, 2024, 2025, 2026]) {
if (anio >= 2022) {
await prisma.isrResicoTasa.createMany({
data: RESICO_TASAS.map(t => ({ anio, montoMaximo: t.montoMaximo, porcentaje: t.porcentaje })),
});
}
const tarifas = ISR_TARIFAS[anio];
if (tarifas) {
await prisma.isrTarifa.createMany({
data: tarifas.map(t => ({
anio,
limiteInferior: t.li,
limiteSuperior: t.ls,
cuotaFija: t.cf,
porcentajeExcedente: t.pe,
})),
});
}
}
console.log('✅ Tablas ISR 2020-2026 cargadas');
// Seed eventos fiscales catálogo
await prisma.eventoFiscalCatalogo.deleteMany();
await prisma.eventoFiscalCatalogo.createMany({
data: EVENTOS_FISCALES.map(e => ({
titulo: e.titulo,
tipo: e.tipo,
diaBase: e.diaBase,
mesRelativo: e.mesRelativo,
mesFijo: (e as any).mesFijo || null,
recurrencia: e.recurrencia,
usaExtensionRfc: e.usaExtensionRfc,
regimenes: e.regimenes,
condicion: e.condicion || null,
})),
});
console.log(`${EVENTOS_FISCALES.length} eventos fiscales cargados`);
// Seed días inhábiles
await prisma.diaInhabil.deleteMany();
await prisma.diaInhabil.createMany({
data: DIAS_INHABILES.map(d => ({ fecha: new Date(d.fecha), nombre: d.nombre })),
skipDuplicates: true,
});
console.log(`${DIAS_INHABILES.length} días inhábiles cargados (2020-2027)`);
// Seed catálogos SAT para facturación
for (const fp of FORMAS_PAGO) {
await prisma.catFormaPago.upsert({ where: { clave: fp.clave }, update: { descripcion: fp.descripcion }, create: fp });
}
console.log(`${FORMAS_PAGO.length} formas de pago cargadas`);
for (const mp of METODOS_PAGO) {
await prisma.catMetodoPago.upsert({ where: { clave: mp.clave }, update: { descripcion: mp.descripcion }, create: mp });
}
console.log(`${METODOS_PAGO.length} métodos de pago cargados`);
for (const u of USOS_CFDI) {
await prisma.catUsoCfdi.upsert({ where: { clave: u.clave }, update: { descripcion: u.descripcion, personaFisica: u.personaFisica, personaMoral: u.personaMoral }, create: u });
}
console.log(`${USOS_CFDI.length} usos CFDI cargados`);
for (const m of MONEDAS) {
await prisma.catMoneda.upsert({ where: { clave: m.clave }, update: { descripcion: m.descripcion, decimales: m.decimales }, create: m });
}
console.log(`${MONEDAS.length} monedas cargadas`);
for (const cu of CLAVES_UNIDAD) {
await prisma.catClaveUnidad.upsert({ where: { clave: cu.clave }, update: { descripcion: cu.descripcion }, create: cu });
}
console.log(`${CLAVES_UNIDAD.length} claves de unidad cargadas`);
for (const oi of OBJETOS_IMP) {
await prisma.catObjetoImp.upsert({ where: { clave: oi.clave }, update: { descripcion: oi.descripcion }, create: oi });
}
console.log(`${OBJETOS_IMP.length} objetos de impuesto cargados`);
for (const tr of TIPOS_RELACION) {
await prisma.catTipoRelacion.upsert({ where: { clave: tr.clave }, update: { descripcion: tr.descripcion }, create: tr });
}
console.log(`${TIPOS_RELACION.length} tipos de relación cargados`);
for (const ex of EXPORTACIONES) {
await prisma.catExportacion.upsert({ where: { clave: ex.clave }, update: { descripcion: ex.descripcion }, create: ex });
}
console.log(`${EXPORTACIONES.length} exportaciones cargadas`);
// Tabla `plan_prices` (modelo PlanPrice) era el catálogo Horux 360 legacy.
// Tras eliminar los planes legacy (2026-04-30), no se siembran filas. Los
// precios despacho viven en `despacho_plan_prices` (modelo DespachoPlanPrice).
// Catálogo despacho — precios + limits. UPSERT idempotente: precios y limits
// se actualizan al re-correr seed (decisión: el seed es source of truth para
// valores iniciales; el admin puede sobreescribir vía UI y NO debe re-correr
// seed si no quiere perder ajustes manuales). Si quieres preservar edits del
// admin, cambiar `update` a `{}` y aplicar manualmente.
const DESPACHO_PLAN_CATALOGO = [
{ plan: 'trial', nombre: 'Prueba', monthly: null, firstYear: null, renewal: null, permiteMonthly: false, maxRfcs: 3, maxUsers: 1, timbresIncluidosMes: 0, dbMode: 'MANAGED' as const, permiteServidorBackup: false, permiteSatIncremental: false },
{ plan: 'custom', nombre: 'Custom', monthly: null, firstYear: null, renewal: null, permiteMonthly: false, maxRfcs: 1, maxUsers: 3, timbresIncluidosMes: 50, dbMode: 'MANAGED' as const, permiteServidorBackup: false, permiteSatIncremental: false },
{ plan: 'mi_empresa', nombre: 'Mi Empresa', monthly: 580, firstYear: 5800, renewal: 5800, permiteMonthly: true, maxRfcs: 1, maxUsers: 3, timbresIncluidosMes: 50, dbMode: 'MANAGED' as const, permiteServidorBackup: false, permiteSatIncremental: false },
{ plan: 'mi_empresa_plus', nombre: 'Mi Empresa +', monthly: 900, firstYear: 9000, renewal: 9000, permiteMonthly: true, maxRfcs: 1, maxUsers: 3, timbresIncluidosMes: 50, dbMode: 'MANAGED' as const, permiteServidorBackup: false, permiteSatIncremental: true },
{ plan: 'business_control', nombre: 'Business Control', monthly: null, firstYear: 25850, renewal: 25850, permiteMonthly: false, maxRfcs: 100, maxUsers: -1, timbresIncluidosMes: 0, dbMode: 'MANAGED' as const, permiteServidorBackup: true, permiteSatIncremental: true },
{ plan: 'business_cloud', nombre: 'Enterprise', monthly: null, firstYear: 43000, renewal: 43000, permiteMonthly: false, maxRfcs: 100, maxUsers: -1, timbresIncluidosMes: 0, dbMode: 'MANAGED' as const, permiteServidorBackup: true, permiteSatIncremental: true },
];
for (const p of DESPACHO_PLAN_CATALOGO) {
await prisma.despachoPlanPrice.upsert({
where: { plan: p.plan },
update: { ...p },
create: { ...p },
});
}
console.log(`${DESPACHO_PLAN_CATALOGO.length} planes despacho cargados (precios + limits)`);
// Catálogo de paquetes de timbres adicionales. Editables desde panel admin.
// Se crean con upsert por `cantidad` (unique) — permite reejecutar seed sin
// sobrescribir precios ya ajustados manualmente: si el row existe, update
// NO toca el precio (solo active + updatedAt si hace falta), sólo lo crea
// si no existía. Si se quiere forzar reset de precios, borrar las filas.
const TIMBRE_PAQUETES = [
{ cantidad: 100, precio: 200 },
{ cantidad: 1000, precio: 1400 },
{ cantidad: 10000, precio: 8600 },
];
for (const p of TIMBRE_PAQUETES) {
await prisma.timbrePaqueteCatalogo.upsert({
where: { cantidad: p.cantidad },
update: {}, // No tocamos `precio` si ya existe (admin pudo editarlo)
create: { cantidad: p.cantidad, precio: p.precio, active: true },
});
}
console.log(`${TIMBRE_PAQUETES.length} paquetes de timbres en catálogo`);
const databaseName = 'horux_ede123456ab1';
// Create demo tenant
const tenant = await prisma.tenant.upsert({
where: { rfc: 'EDE123456AB1' },
update: {},
create: {
nombre: 'Empresa Demo SA de CV',
rfc: 'EDE123456AB1',
plan: 'mi_empresa_plus',
databaseName,
},
});
console.log('✅ Tenant created:', tenant.nombre);
// Migración: renombra el rol legacy 'admin' a 'owner' si sobrevive de un seed viejo.
// Idempotente (no-op si ya se renombró o nunca existió).
await prisma.$executeRawUnsafe(`UPDATE roles SET nombre = 'owner' WHERE nombre = 'admin'`);
// Backfill de trial_usages para tenants que ya consumieron su trial antes de que
// existiera esta tabla. Idempotente: ON CONFLICT DO NOTHING. Filtramos por
// longitud porque trial_usages.rfc es varchar(13) y los tenants despacho usan
// slugs largos (DESPACHO_xxx) que no encajan — el padrón anti-abuso de trial
// solo aplica a RFCs SAT reales de personas/empresas, no a slugs.
await prisma.$executeRawUnsafe(`
INSERT INTO trial_usages (rfc, tenant_id, started_at)
SELECT UPPER(rfc), id, COALESCE(created_at, NOW())
FROM tenants
WHERE trial_ends_at IS NOT NULL AND LENGTH(rfc) <= 13
ON CONFLICT (rfc) DO NOTHING
`);
// Backfill de user_platform_roles: los owners del tenant HTS240708LJA se
// convierten automáticamente en platform_admin. Migrado a tenant_memberships
// tras F6 (User.tenantId/rolId eliminados). Idempotente.
await prisma.$executeRawUnsafe(`
INSERT INTO user_platform_roles (user_id, role, created_at)
SELECT tm.user_id, 'platform_admin'::"PlatformRole", NOW()
FROM tenant_memberships tm
JOIN tenants t ON tm.tenant_id = t.id
WHERE t.rfc = 'HTS240708LJA' AND tm.is_owner = true AND tm.active = true
ON CONFLICT (user_id, role) DO NOTHING
`);
// (Backfill de tenant_memberships eliminado — F6 ya migró todos los users
// legacy y los campos `User.tenantId` y `User.rolId` ya no existen. Los
// users nuevos se crean directamente con su membership.)
// Seed roles
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' },
];
for (const r of rolesData) {
await prisma.rol.upsert({
where: { nombre: r.nombre },
update: { descripcion: r.descripcion },
create: r,
});
}
// Seed despacho roles
await prisma.rol.upsert({
where: { nombre: 'supervisor' },
update: {},
create: { id: 9, nombre: 'supervisor', descripcion: 'Supervisor de despacho — titular de RFCs, crea carteras' },
});
await prisma.rol.upsert({
where: { nombre: 'cliente' },
update: {},
create: { id: 10, nombre: 'cliente', descripcion: 'Cliente visor externo — acceso read-only a sus RFCs' },
});
const roles = await prisma.rol.findMany();
const rolMap = new Map(roles.map(r => [r.nombre, r.id]));
console.log(`${roles.length} roles cargados`);
// Create demo users
const passwordHash = await bcrypt.hash('demo123', 12);
const users = [
{ email: 'admin@demo.com', nombre: 'Dueño Demo', rolNombre: 'owner' },
{ email: 'contador@demo.com', nombre: 'Contador Demo', rolNombre: 'contador' },
{ email: 'visor@demo.com', nombre: 'Visor Demo', rolNombre: 'visor' },
];
for (const userData of users) {
const rolId = rolMap.get(userData.rolNombre)!;
const user = await prisma.user.upsert({
where: { email: userData.email },
update: {},
create: {
email: userData.email,
passwordHash,
nombre: userData.nombre,
lastTenantId: tenant.id,
},
});
// Membership al tenant demo (idempotente — F6 multi-tenant: la autorización
// vive en tenant_memberships, no en User.tenantId/rolId).
await prisma.tenantMembership.upsert({
where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } },
update: { rolId, isOwner: userData.rolNombre === 'owner' || userData.rolNombre === 'cfo', active: true },
create: {
userId: user.id,
tenantId: tenant.id,
rolId,
isOwner: userData.rolNombre === 'owner' || userData.rolNombre === 'cfo',
active: true,
},
});
console.log(`✅ User created: ${user.email} (${userData.rolNombre})`);
}
// Create tenant database
const dbConfig = parseDatabaseUrl(process.env.DATABASE_URL!);
const adminPool = new Pool({ ...dbConfig, database: 'postgres', max: 1 });
try {
const exists = await adminPool.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[databaseName]
);
if (exists.rows.length === 0) {
await adminPool.query(`CREATE DATABASE "${databaseName}"`);
console.log(`✅ Tenant database created: ${databaseName}`);
} else {
console.log(` Tenant database already exists: ${databaseName}`);
}
} finally {
await adminPool.end();
}
// Create tables in tenant database
const tenantPool = new Pool({ ...dbConfig, database: databaseName, max: 1 });
try {
// Reset tenant tables so the re-seed parte de cero. Luego corremos las
// migraciones (fuente única de verdad del schema tenant) para garantizar
// que queden todas las tablas y columnas actuales.
await tenantPool.query(`
DROP TABLE IF EXISTS cfdi_conceptos CASCADE;
DROP TABLE IF EXISTS cfdis CASCADE;
DROP TABLE IF EXISTS conciliaciones CASCADE;
DROP TABLE IF EXISTS bancos CASCADE;
DROP TABLE IF EXISTS recordatorios CASCADE;
DROP TABLE IF EXISTS alertas CASCADE;
DROP TABLE IF EXISTS rfcs CASCADE;
DROP TABLE IF EXISTS opiniones_cumplimiento CASCADE;
DROP TABLE IF EXISTS schema_migrations;
`);
await migrate(tenantPool, tenant.rfc);
console.log('✅ Tenant schema aplicado vía migraciones');
// Bloque legacy de CREATE TABLE / CREATE INDEX retirado: vive ahora en
// `apps/api/src/migrations/tenant/*.sql` (fuente única de verdad).
// Insert demo CFDIs with new structure
const cfdiTypes = ['EMITIDO', 'RECIBIDO'];
const tipoComprobantes: Record<string, string> = { EMITIDO: 'I', RECIBIDO: 'I' };
const rfcs = ['XAXX010101000', 'MEXX020202000', 'AAXX030303000', 'BBXX040404000'];
const nombres = ['Cliente Demo SA', 'Proveedor ABC', 'Servicios XYZ', 'Materiales 123'];
for (let i = 0; i < 50; i++) {
const tipo = cfdiTypes[i % 2];
const rfcIndex = i % 4;
const subtotal = Math.floor(Math.random() * 50000) + 1000;
const iva = subtotal * 0.16;
const total = subtotal + iva;
const daysAgo = Math.floor(Math.random() * 180);
const fecha = new Date();
fecha.setDate(fecha.getDate() - daysAgo);
const year = String(fecha.getFullYear());
const month = String(fecha.getMonth() + 1).padStart(2, '0');
// Sin ON CONFLICT: las tablas se dropean en línea 342-352 antes de seed
// y los UUIDs son crypto.randomUUID() (probabilidad de colisión ~0).
// El UNIQUE en cfdis es funcional (LOWER(uuid)), no acepta ON CONFLICT
// por columna plana — ver migración 027_cfdi_uuid_unique_case_insensitive.
await tenantPool.query(`
INSERT INTO cfdis (
year, month, type, uuid, serie, folio, status, fecha_emision,
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
subtotal, subtotal_mxn, descuento, descuento_mxn,
total, total_mxn, moneda, tipo_cambio, tipo_comprobante,
metodo_pago, iva_traslado, iva_traslado_mxn,
regimen_fiscal_emisor, regimen_fiscal_receptor
) 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)
`, [
year, month, tipo, crypto.randomUUID(), 'A', String(1000 + i),
'Vigente', fecha,
tipo === 'EMITIDO' ? 'EDE123456AB1' : rfcs[rfcIndex],
tipo === 'EMITIDO' ? 'Empresa Demo SA de CV' : nombres[rfcIndex],
tipo === 'RECIBIDO' ? 'EDE123456AB1' : rfcs[rfcIndex],
tipo === 'RECIBIDO' ? 'Empresa Demo SA de CV' : nombres[rfcIndex],
subtotal, subtotal, 0, 0,
total, total, 'MXN', 1, tipoComprobantes[tipo],
'PUE', iva, iva,
'601', '601',
]);
}
console.log('✅ Demo CFDIs created (50)');
// Insert demo conceptos for each CFDI
const { rows: allCfdis } = await tenantPool.query(`SELECT id FROM cfdis`);
const productos = ['Servicio de consultoría', 'Licencia de software', 'Soporte técnico', 'Desarrollo web', 'Capacitación'];
for (const c of allCfdis) {
const numConceptos = Math.floor(Math.random() * 3) + 1;
for (let j = 0; j < numConceptos; j++) {
const cantidad = Math.floor(Math.random() * 5) + 1;
const valorUnitario = Math.floor(Math.random() * 5000) + 500;
const importe = cantidad * valorUnitario;
const iva = importe * 0.16;
await tenantPool.query(`
INSERT INTO cfdi_conceptos (
cfdi_id, clave_prod_serv, descripcion, cantidad, clave_unidad, unidad,
valor_unitario, valor_unitario_mxn,
importe, importe_mxn, descuento, descuento_mxn,
iva_traslado, iva_traslado_mxn
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
`, [
c.id,
'84111506', productos[j % productos.length], cantidad, 'E48', 'Servicio',
valorUnitario, valorUnitario,
importe, importe, 0, 0,
iva, iva,
]);
}
}
console.log('✅ Demo conceptos created');
} finally {
await tenantPool.end();
}
// (PlanCatalogo seed eliminado — el modelo se dropeó en migración
// 20260430200000_drop_plan_catalogo_orphan; el catálogo despacho vive en
// `despacho_plan_prices` y se siembra arriba en DESPACHO_PLAN_CATALOGO.)
// Seed addon catalog
const addonCatalogoData = [
{
codename: 'rfcs_extra_10',
nombre: '+10 RFCs adicionales',
verticalProfile: 'CONTABLE' as const,
precio: 190,
frecuencia: 'mensual',
delta: { maxRfcs: 10 },
},
{
codename: 'rfcs_extra_50',
nombre: '+50 RFCs adicionales',
verticalProfile: 'CONTABLE' as const,
precio: 690,
frecuencia: 'mensual',
delta: { maxRfcs: 50 },
},
{
codename: 'timbres_extra_500',
nombre: '+500 timbres mensuales',
precio: 490,
frecuencia: 'mensual',
delta: { timbresIncluidosMes: 500 },
},
{
codename: 'modulo_ia',
nombre: 'Módulo IA Fiscal',
precio: 390,
frecuencia: 'mensual',
delta: { features: ['ia_lolita'] },
},
{
// Lolita IA activable por contribuyente específico del despacho.
// SubscriptionAddon.contribuyenteId apunta al RFC que lo contrata.
// Cobro mensual en preapproval propio (la licencia del despacho es anual;
// el add-on va en ciclo independiente).
codename: 'lolita_ia_contribuyente',
nombre: 'Lolita IA (por contribuyente)',
verticalProfile: 'CONTABLE' as const,
precio: 250,
frecuencia: 'mensual',
delta: { features: ['ia_lolita'] },
},
{
// Contribuyente adicional para planes Business Control y Enterprise
// (ambos incluyen 100 base). Se cobra automáticamente según overage; no
// requiere opt-in, pero se modela como add-on para que el preapproval MP
// lo cubra. El codename mantiene el sufijo "business_cloud" por compat
// con suscripciones existentes; el nombre display ya es genérico.
codename: 'contribuyente_extra_business_cloud',
nombre: 'Contribuyente adicional (RFC extra)',
verticalProfile: 'CONTABLE' as const,
precio: 45,
frecuencia: 'mensual',
delta: { maxRfcs: 1 },
},
];
for (const a of addonCatalogoData) {
await prisma.planAddonCatalogo.upsert({
where: { codename: a.codename },
update: { nombre: a.nombre, precio: a.precio, delta: a.delta },
create: { ...a, verticalProfile: a.verticalProfile ?? null },
});
}
console.log('✓ Addon catalog seeded (6 addons)');
console.log('\n🎉 Seed completed successfully!');
console.log('\n📝 Demo credentials:');
console.log(' Admin: admin@demo.com / demo123');
console.log(' Contador: contador@demo.com / demo123');
console.log(' Visor: visor@demo.com / demo123');
}
main()
.catch((e) => {
console.error('Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});