Update: nueva version Horux Despachos
This commit is contained in:
559
apps/api/prisma/seed.ts
Normal file
559
apps/api/prisma/seed.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
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`);
|
||||
|
||||
// Seed precios de planes (editables vía BD — custom no se incluye, se fija por tenant)
|
||||
const PLAN_PRICES = [
|
||||
{ plan: 'starter' as const, frequency: 'monthly', amount: 199 },
|
||||
{ plan: 'starter' as const, frequency: 'annual', amount: 1990 },
|
||||
{ plan: 'business' as const, frequency: 'monthly', amount: 480 },
|
||||
{ plan: 'business' as const, frequency: 'annual', amount: 4800 },
|
||||
{ plan: 'business_ia' as const, frequency: 'monthly', amount: 780 },
|
||||
{ plan: 'business_ia' as const, frequency: 'annual', amount: 7800 },
|
||||
{ plan: 'enterprise' as const, frequency: 'monthly', amount: 900 },
|
||||
{ plan: 'enterprise' as const, frequency: 'annual', amount: 9000 },
|
||||
];
|
||||
for (const p of PLAN_PRICES) {
|
||||
await prisma.planPrice.upsert({
|
||||
where: { plan_frequency: { plan: p.plan, frequency: p.frequency } },
|
||||
update: { amount: p.amount },
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log(`✅ ${PLAN_PRICES.length} precios de planes cargados`);
|
||||
|
||||
// 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: 'business',
|
||||
databaseName,
|
||||
cfdiLimit: 500,
|
||||
usersLimit: 3,
|
||||
},
|
||||
});
|
||||
|
||||
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.
|
||||
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
|
||||
ON CONFLICT (rfc) DO NOTHING
|
||||
`);
|
||||
|
||||
// Backfill de user_platform_roles: los owners del tenant HTS240708LJA se
|
||||
// convierten automáticamente en platform_admin. Esto preserva el comportamiento
|
||||
// anterior (admin global por RFC) al mismo tiempo que abre la puerta al modelo
|
||||
// de roles granulares. Idempotente.
|
||||
await prisma.$executeRawUnsafe(`
|
||||
INSERT INTO user_platform_roles (user_id, role, created_at)
|
||||
SELECT u.id, 'platform_admin'::"PlatformRole", NOW()
|
||||
FROM users u
|
||||
JOIN tenants t ON u.tenant_id = t.id
|
||||
JOIN roles r ON u.rol_id = r.id
|
||||
WHERE t.rfc = 'HTS240708LJA' AND r.nombre = 'owner'
|
||||
ON CONFLICT (user_id, role) DO NOTHING
|
||||
`);
|
||||
|
||||
// Backfill de tenant_memberships: cada user existente genera una membership
|
||||
// con su tenant y rol actuales. isOwner = true si su rol es 'owner' (u 'cfo',
|
||||
// que es equivalente en permisos). Esto es el fundamento del modelo multi-tenant
|
||||
// — durante la transición, User.tenantId sigue siendo el "default tenant" para
|
||||
// login UX, pero las autorizaciones verdaderas vienen de esta tabla.
|
||||
// Idempotente: ON CONFLICT evita duplicados al re-correr seed.
|
||||
await prisma.$executeRawUnsafe(`
|
||||
INSERT INTO tenant_memberships (user_id, tenant_id, rol_id, is_owner, active, joined_at)
|
||||
SELECT u.id, u.tenant_id, u.rol_id, (r.nombre IN ('owner', 'cfo')), u.active, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON u.rol_id = r.id
|
||||
ON CONFLICT (user_id, tenant_id) DO NOTHING
|
||||
`);
|
||||
|
||||
// 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 user = await prisma.user.upsert({
|
||||
where: { email: userData.email },
|
||||
update: {},
|
||||
create: {
|
||||
tenantId: tenant.id,
|
||||
email: userData.email,
|
||||
passwordHash,
|
||||
nombre: userData.nombre,
|
||||
rolId: rolMap.get(userData.rolNombre)!,
|
||||
},
|
||||
include: { rol: true },
|
||||
});
|
||||
console.log(`✅ User created: ${user.email} (${user.rol.nombre})`);
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
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)
|
||||
ON CONFLICT (uuid) DO NOTHING
|
||||
`, [
|
||||
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();
|
||||
}
|
||||
|
||||
// Seed plan catalog for CONTABLE vertical
|
||||
const planCatalogoData = [
|
||||
{
|
||||
codename: 'trial_contable',
|
||||
nombre: 'Trial Contable',
|
||||
verticalProfile: 'CONTABLE' as const,
|
||||
precioBase: 0,
|
||||
frecuencia: 'mensual',
|
||||
limits: { maxRfcs: 3, maxUsers: 1, timbresIncluidosMes: 20, features: ['dashboard', 'cfdi_basic', 'iva_isr'] },
|
||||
},
|
||||
{
|
||||
codename: 'starter_contable',
|
||||
nombre: 'Starter Contable',
|
||||
verticalProfile: 'CONTABLE' as const,
|
||||
precioBase: 490,
|
||||
frecuencia: 'mensual',
|
||||
limits: { maxRfcs: 10, maxUsers: 3, timbresIncluidosMes: 50, features: ['dashboard', 'cfdi_basic', 'iva_isr', 'alertas', 'calendario'] },
|
||||
},
|
||||
{
|
||||
codename: 'business_contable',
|
||||
nombre: 'Business Contable',
|
||||
verticalProfile: 'CONTABLE' as const,
|
||||
precioBase: 1290,
|
||||
frecuencia: 'mensual',
|
||||
limits: { maxRfcs: 50, maxUsers: 10, timbresIncluidosMes: 200, features: ['dashboard', 'cfdi_basic', 'iva_isr', 'alertas', 'calendario', 'reportes', 'conciliacion', 'documentos', 'facturacion'] },
|
||||
},
|
||||
{
|
||||
codename: 'enterprise_contable',
|
||||
nombre: 'Enterprise Contable',
|
||||
verticalProfile: 'CONTABLE' as const,
|
||||
precioBase: 2990,
|
||||
frecuencia: 'mensual',
|
||||
limits: { maxRfcs: -1, maxUsers: -1, timbresIncluidosMes: 600, features: ['dashboard', 'cfdi_basic', 'iva_isr', 'alertas', 'calendario', 'reportes', 'conciliacion', 'documentos', 'facturacion', 'api'] },
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of planCatalogoData) {
|
||||
await prisma.planCatalogo.upsert({
|
||||
where: { codename: p.codename },
|
||||
update: { nombre: p.nombre, precioBase: p.precioBase, limits: p.limits },
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log('✓ Plan catalog seeded (4 plans CONTABLE)');
|
||||
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user