Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
// Catálogos SAT CFDI 4.0 para facturación
export const FORMAS_PAGO = [
{ clave: '01', descripcion: 'Efectivo' },
{ clave: '02', descripcion: 'Cheque nominativo' },
{ clave: '03', descripcion: 'Transferencia electrónica de fondos' },
{ clave: '04', descripcion: 'Tarjeta de crédito' },
{ clave: '05', descripcion: 'Monedero electrónico' },
{ clave: '06', descripcion: 'Dinero electrónico' },
{ clave: '08', descripcion: 'Vales de despensa' },
{ clave: '12', descripcion: 'Dación en pago' },
{ clave: '13', descripcion: 'Pago por subrogación' },
{ clave: '14', descripcion: 'Pago por consignación' },
{ clave: '15', descripcion: 'Condonación' },
{ clave: '17', descripcion: 'Compensación' },
{ clave: '23', descripcion: 'Novación' },
{ clave: '24', descripcion: 'Confusión' },
{ clave: '25', descripcion: 'Remisión de deuda' },
{ clave: '26', descripcion: 'Prescripción o caducidad' },
{ clave: '27', descripcion: 'A satisfacción del acreedor' },
{ clave: '28', descripcion: 'Tarjeta de débito' },
{ clave: '29', descripcion: 'Tarjeta de servicios' },
{ clave: '30', descripcion: 'Aplicación de anticipos' },
{ clave: '31', descripcion: 'Intermediario pagos' },
{ clave: '99', descripcion: 'Por definir' },
];
export const METODOS_PAGO = [
{ clave: 'PUE', descripcion: 'Pago en una sola exhibición' },
{ clave: 'PPD', descripcion: 'Pago en parcialidades o diferido' },
];
export const USOS_CFDI = [
{ clave: 'G01', descripcion: 'Adquisición de mercancías', personaFisica: true, personaMoral: true },
{ clave: 'G02', descripcion: 'Devoluciones, descuentos o bonificaciones', personaFisica: true, personaMoral: true },
{ clave: 'G03', descripcion: 'Gastos en general', personaFisica: true, personaMoral: true },
{ clave: 'I01', descripcion: 'Construcciones', personaFisica: true, personaMoral: true },
{ clave: 'I02', descripcion: 'Mobiliario y equipo de oficina por inversiones', personaFisica: true, personaMoral: true },
{ clave: 'I03', descripcion: 'Equipo de transporte', personaFisica: true, personaMoral: true },
{ clave: 'I04', descripcion: 'Equipo de cómputo y accesorios', personaFisica: true, personaMoral: true },
{ clave: 'I05', descripcion: 'Dados, troqueles, moldes, matrices y herramental', personaFisica: true, personaMoral: true },
{ clave: 'I06', descripcion: 'Comunicaciones telefónicas', personaFisica: true, personaMoral: true },
{ clave: 'I07', descripcion: 'Comunicaciones satelitales', personaFisica: true, personaMoral: true },
{ clave: 'I08', descripcion: 'Otra maquinaria y equipo', personaFisica: true, personaMoral: true },
{ clave: 'D01', descripcion: 'Honorarios médicos, dentales y gastos hospitalarios', personaFisica: true, personaMoral: false },
{ clave: 'D02', descripcion: 'Gastos médicos por incapacidad o discapacidad', personaFisica: true, personaMoral: false },
{ clave: 'D03', descripcion: 'Gastos funerales', personaFisica: true, personaMoral: false },
{ clave: 'D04', descripcion: 'Donativos', personaFisica: true, personaMoral: true },
{ clave: 'D05', descripcion: 'Intereses reales efectivamente pagados por créditos hipotecarios', personaFisica: true, personaMoral: false },
{ clave: 'D06', descripcion: 'Aportaciones voluntarias al SAR', personaFisica: true, personaMoral: false },
{ clave: 'D07', descripcion: 'Primas por seguros de gastos médicos', personaFisica: true, personaMoral: false },
{ clave: 'D08', descripcion: 'Gastos de transportación escolar obligatoria', personaFisica: true, personaMoral: false },
{ clave: 'D09', descripcion: 'Depósitos en cuentas para el ahorro, primas de pensiones', personaFisica: true, personaMoral: false },
{ clave: 'D10', descripcion: 'Pagos por servicios educativos (colegiaturas)', personaFisica: true, personaMoral: false },
{ clave: 'S01', descripcion: 'Sin efectos fiscales', personaFisica: true, personaMoral: true },
{ clave: 'CP01', descripcion: 'Pagos', personaFisica: true, personaMoral: true },
{ clave: 'CN01', descripcion: 'Nómina', personaFisica: true, personaMoral: false },
];
export const MONEDAS = [
{ clave: 'MXN', descripcion: 'Peso Mexicano', decimales: 2 },
{ clave: 'USD', descripcion: 'Dólar Americano', decimales: 2 },
{ clave: 'EUR', descripcion: 'Euro', decimales: 2 },
{ clave: 'GBP', descripcion: 'Libra Esterlina', decimales: 2 },
{ clave: 'CAD', descripcion: 'Dólar Canadiense', decimales: 2 },
{ clave: 'JPY', descripcion: 'Yen Japonés', decimales: 0 },
{ clave: 'XXX', descripcion: 'Los códigos asignados para transacciones en que intervenga ninguna moneda', decimales: 0 },
];
export const CLAVES_UNIDAD = [
{ clave: 'H87', descripcion: 'Pieza' },
{ clave: 'E48', descripcion: 'Unidad de servicio' },
{ clave: 'KGM', descripcion: 'Kilogramo' },
{ clave: 'LTR', descripcion: 'Litro' },
{ clave: 'MTR', descripcion: 'Metro' },
{ clave: 'MTK', descripcion: 'Metro cuadrado' },
{ clave: 'MTQ', descripcion: 'Metro cúbico' },
{ clave: 'KWH', descripcion: 'Kilovatio hora' },
{ clave: 'TNE', descripcion: 'Tonelada' },
{ clave: 'GRM', descripcion: 'Gramo' },
{ clave: 'HUR', descripcion: 'Hora' },
{ clave: 'DAY', descripcion: 'Día' },
{ clave: 'MON', descripcion: 'Mes' },
{ clave: 'ANN', descripcion: 'Año' },
{ clave: 'XBX', descripcion: 'Caja' },
{ clave: 'XPK', descripcion: 'Paquete' },
{ clave: 'XKI', descripcion: 'Kit' },
{ clave: 'SET', descripcion: 'Conjunto' },
{ clave: 'XLT', descripcion: 'Lote' },
{ clave: 'ACT', descripcion: 'Actividad' },
{ clave: 'XUN', descripcion: 'Unidad' },
{ clave: 'DPC', descripcion: 'Docena de piezas' },
{ clave: 'XRO', descripcion: 'Rollo' },
{ clave: 'GLL', descripcion: 'Galón' },
{ clave: 'MLT', descripcion: 'Mililitro' },
{ clave: 'CMT', descripcion: 'Centímetro' },
];
export const OBJETOS_IMP = [
{ clave: '01', descripcion: 'No objeto de impuesto' },
{ clave: '02', descripcion: 'Sí objeto de impuesto' },
{ clave: '03', descripcion: 'Sí objeto del impuesto y no obligado al desglose' },
{ clave: '04', descripcion: 'Sí objeto del impuesto y no causa impuesto' },
];
export const TIPOS_RELACION = [
{ clave: '01', descripcion: 'Nota de crédito de los documentos relacionados' },
{ clave: '02', descripcion: 'Nota de débito de los documentos relacionados' },
{ clave: '03', descripcion: 'Devolución de mercancía sobre facturas o traslados previos' },
{ clave: '04', descripcion: 'Sustitución de los CFDI previos' },
{ clave: '05', descripcion: 'Traslados de mercancías facturados previamente' },
{ clave: '06', descripcion: 'Factura generada por los traslados previos' },
{ clave: '07', descripcion: 'CFDI por aplicación de anticipo' },
];
export const EXPORTACIONES = [
{ clave: '01', descripcion: 'No aplica' },
{ clave: '02', descripcion: 'Definitiva' },
{ clave: '03', descripcion: 'Temporal' },
{ clave: '04', descripcion: 'Definitiva con clave distinta a A1 o cuando no existe enajenación en términos del CFF' },
];

View File

@@ -0,0 +1,185 @@
// Catálogo de eventos fiscales
export const EVENTOS_FISCALES = [
{
titulo: 'Declaración mensual ISR',
tipo: 'declaracion',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'Declaración mensual IVA',
tipo: 'declaracion',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'Declaración mensual IEPS',
tipo: 'declaracion',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'Declaración de sueldos y salarios',
tipo: 'declaracion',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: 'todos',
condicion: 'tiene_nomina',
},
{
titulo: 'Pago provisional ISR',
tipo: 'pago',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: true,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'Pago provisional IVA',
tipo: 'pago',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: true,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'Pago provisional IEPS',
tipo: 'pago',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: true,
regimenes: 'todos',
condicion: null,
},
{
titulo: 'DIOT',
tipo: 'obligacion',
diaBase: 17,
mesRelativo: 1,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: '601,603,607,608,610,611,612,614,615,620,622,623,624',
condicion: null, // 612 aplica condición ingresos_4m, se valida en runtime
},
{
titulo: 'Contabilidad electrónica',
tipo: 'obligacion',
diaBase: 3,
mesRelativo: 2,
recurrencia: 'mensual',
usaExtensionRfc: false,
regimenes: '601,603,607,608,610,611,612,614,615,620,622,623,624',
condicion: null,
},
{
titulo: 'Declaración anual PM',
tipo: 'declaracion',
diaBase: 31,
mesRelativo: 0,
mesFijo: 3,
recurrencia: 'anual',
usaExtensionRfc: false,
regimenes: '601,603,620,622,623,624',
condicion: null,
},
{
titulo: 'Declaración anual PF',
tipo: 'declaracion',
diaBase: 30,
mesRelativo: 0,
mesFijo: 4,
recurrencia: 'anual',
usaExtensionRfc: false,
regimenes: '605,606,607,608,611,612,614,615,621,625,626',
condicion: null,
},
{
titulo: 'Informativa Sueldos y Salarios',
tipo: 'informativa',
diaBase: 15,
mesRelativo: 0,
mesFijo: 2,
recurrencia: 'anual',
usaExtensionRfc: false,
regimenes: 'todos',
condicion: 'tiene_nomina',
},
];
// Días festivos oficiales de México (2020-2027)
// Incluye: 1 ene, 5 feb, 21 mar, 1 may, 16 sep, 1 oct (cambio poder), 20 nov, 25 dic
// + cambios de poder cada 6 años, semana santa variable
export const DIAS_INHABILES: { fecha: string; nombre: string }[] = [];
function addFestivos(año: number) {
const fijos = [
{ mes: 1, dia: 1, nombre: 'Año Nuevo' },
{ mes: 5, dia: 1, nombre: 'Día del Trabajo' },
{ mes: 9, dia: 16, nombre: 'Independencia de México' },
{ mes: 12, dia: 25, nombre: 'Navidad' },
];
// Primer lunes de febrero (Constitución)
const feb1 = new Date(año, 1, 1);
const primerLunesFeb = new Date(año, 1, 1 + ((8 - feb1.getDay()) % 7));
DIAS_INHABILES.push({
fecha: primerLunesFeb.toISOString().split('T')[0],
nombre: 'Día de la Constitución',
});
// Tercer lunes de marzo (Benito Juárez)
const mar1 = new Date(año, 2, 1);
const primerLunesMar = new Date(año, 2, 1 + ((8 - mar1.getDay()) % 7));
const tercerLunesMar = new Date(primerLunesMar);
tercerLunesMar.setDate(tercerLunesMar.getDate() + 14);
DIAS_INHABILES.push({
fecha: tercerLunesMar.toISOString().split('T')[0],
nombre: 'Natalicio de Benito Juárez',
});
// Tercer lunes de noviembre (Revolución)
const nov1 = new Date(año, 10, 1);
const primerLunesNov = new Date(año, 10, 1 + ((8 - nov1.getDay()) % 7));
const tercerLunesNov = new Date(primerLunesNov);
tercerLunesNov.setDate(tercerLunesNov.getDate() + 14);
DIAS_INHABILES.push({
fecha: tercerLunesNov.toISOString().split('T')[0],
nombre: 'Día de la Revolución',
});
for (const f of fijos) {
DIAS_INHABILES.push({
fecha: `${año}-${String(f.mes).padStart(2, '0')}-${String(f.dia).padStart(2, '0')}`,
nombre: f.nombre,
});
}
// Cambio de poder (1 oct cada 6 años: 2024, 2030...)
if (año % 6 === 0 || (año - 2024) % 6 === 0) {
DIAS_INHABILES.push({
fecha: `${año}-10-01`,
nombre: 'Transmisión del Poder Ejecutivo Federal',
});
}
}
for (let y = 2020; y <= 2027; y++) addFestivos(y);

103
apps/api/prisma/isr-data.ts Normal file
View File

@@ -0,0 +1,103 @@
// Tasas RESICO (Art. 113-E) - iguales 2022-2026
export const RESICO_TASAS = [
{ montoMaximo: 25000.00, porcentaje: 1.00 },
{ montoMaximo: 50000.00, porcentaje: 1.10 },
{ montoMaximo: 83888.33, porcentaje: 1.50 },
{ montoMaximo: 208333.33, porcentaje: 2.00 },
{ montoMaximo: 291666.66, porcentaje: 2.50 },
];
// Tarifas ISR mensuales (Art. 96) por año
export const ISR_TARIFAS: Record<number, { li: number; ls: number | null; cf: number; pe: number }[]> = {
2020: [
{ li: 0.01, ls: 578.52, cf: 0, pe: 1.92 },
{ li: 578.53, ls: 4910.18, cf: 11.11, pe: 6.40 },
{ li: 4910.19, ls: 8629.20, cf: 288.33, pe: 10.88 },
{ li: 8629.21, ls: 10031.07, cf: 692.96, pe: 16.00 },
{ li: 10031.08, ls: 12009.94, cf: 917.26, pe: 17.92 },
{ li: 12009.95, ls: 24222.31, cf: 1271.87, pe: 21.36 },
{ li: 24222.32, ls: 38177.69, cf: 3880.44, pe: 23.52 },
{ li: 38177.70, ls: 72887.50, cf: 7162.74, pe: 30.00 },
{ li: 72887.51, ls: 97183.33, cf: 17575.69, pe: 32.00 },
{ li: 97183.34, ls: 291550.00, cf: 25350.35, pe: 34.00 },
{ li: 291550.01, ls: null, cf: 91435.02, pe: 35.00 },
],
2021: [
{ li: 0.01, ls: 644.58, cf: 0, pe: 1.92 },
{ li: 644.59, ls: 5470.92, cf: 12.38, pe: 6.40 },
{ li: 5470.93, ls: 9614.66, cf: 321.26, pe: 10.88 },
{ li: 9614.67, ls: 11176.62, cf: 772.10, pe: 16.00 },
{ li: 11176.63, ls: 13381.47, cf: 1022.01, pe: 17.92 },
{ li: 13381.48, ls: 26988.50, cf: 1417.12, pe: 21.36 },
{ li: 26988.51, ls: 42537.58, cf: 4323.58, pe: 23.52 },
{ li: 42537.59, ls: 81211.25, cf: 7980.73, pe: 30.00 },
{ li: 81211.26, ls: 108281.67, cf: 19582.83, pe: 32.00 },
{ li: 108281.68, ls: 324845.01, cf: 28245.36, pe: 34.00 },
{ li: 324845.02, ls: null, cf: 101876.90, pe: 35.00 },
],
2022: [
{ li: 0.01, ls: 644.58, cf: 0, pe: 1.92 },
{ li: 644.59, ls: 5470.92, cf: 12.38, pe: 6.40 },
{ li: 5470.93, ls: 9614.66, cf: 321.26, pe: 10.88 },
{ li: 9614.67, ls: 11176.62, cf: 772.10, pe: 16.00 },
{ li: 11176.63, ls: 13381.47, cf: 1022.01, pe: 17.92 },
{ li: 13381.48, ls: 26988.50, cf: 1417.12, pe: 21.36 },
{ li: 26988.51, ls: 42537.58, cf: 4323.58, pe: 23.52 },
{ li: 42537.59, ls: 81211.25, cf: 7980.73, pe: 30.00 },
{ li: 81211.26, ls: 108281.67, cf: 19582.83, pe: 32.00 },
{ li: 108281.68, ls: 324845.01, cf: 28245.36, pe: 34.00 },
{ li: 324845.02, ls: null, cf: 101876.90, pe: 35.00 },
],
2023: [
{ li: 0.01, ls: 746.04, cf: 0, pe: 1.92 },
{ li: 746.05, ls: 6332.05, cf: 14.32, pe: 6.40 },
{ li: 6332.06, ls: 11128.01, cf: 371.83, pe: 10.88 },
{ li: 11128.02, ls: 12935.82, cf: 893.63, pe: 16.00 },
{ li: 12935.83, ls: 15487.71, cf: 1182.88, pe: 17.92 },
{ li: 15487.72, ls: 31236.49, cf: 1640.18, pe: 21.36 },
{ li: 31236.50, ls: 49233.00, cf: 5004.12, pe: 23.52 },
{ li: 49233.01, ls: 93993.90, cf: 9236.89, pe: 30.00 },
{ li: 93993.91, ls: 125325.20, cf: 22665.17, pe: 32.00 },
{ li: 125325.21, ls: 375975.61, cf: 32691.18, pe: 34.00 },
{ li: 375975.62, ls: null, cf: 117912.32, pe: 35.00 },
],
2024: [
{ li: 0.01, ls: 746.04, cf: 0, pe: 1.92 },
{ li: 746.05, ls: 6332.05, cf: 14.32, pe: 6.40 },
{ li: 6332.06, ls: 11128.01, cf: 371.83, pe: 10.88 },
{ li: 11128.02, ls: 12935.82, cf: 893.63, pe: 16.00 },
{ li: 12935.83, ls: 15487.71, cf: 1182.88, pe: 17.92 },
{ li: 15487.72, ls: 31236.49, cf: 1640.18, pe: 21.36 },
{ li: 31236.50, ls: 49233.00, cf: 5004.12, pe: 23.52 },
{ li: 49233.01, ls: 93993.90, cf: 9236.89, pe: 30.00 },
{ li: 93993.91, ls: 125325.20, cf: 22665.17, pe: 32.00 },
{ li: 125325.21, ls: 375975.61, cf: 32691.18, pe: 34.00 },
{ li: 375975.62, ls: null, cf: 117912.32, pe: 35.00 },
],
2025: [
{ li: 0.01, ls: 746.04, cf: 0, pe: 1.92 },
{ li: 746.05, ls: 6332.05, cf: 14.32, pe: 6.40 },
{ li: 6332.06, ls: 11128.01, cf: 371.83, pe: 10.88 },
{ li: 11128.02, ls: 12935.82, cf: 893.63, pe: 16.00 },
{ li: 12935.83, ls: 15487.71, cf: 1182.88, pe: 17.92 },
{ li: 15487.72, ls: 31236.49, cf: 1640.18, pe: 21.36 },
{ li: 31236.50, ls: 49233.00, cf: 5004.12, pe: 23.52 },
{ li: 49233.01, ls: 93993.90, cf: 9236.89, pe: 30.00 },
{ li: 93993.91, ls: 125325.20, cf: 22665.17, pe: 32.00 },
{ li: 125325.21, ls: 375975.61, cf: 32691.18, pe: 34.00 },
{ li: 375975.62, ls: null, cf: 117912.32, pe: 35.00 },
],
2026: [
{ li: 0.01, ls: 844.59, cf: 0, pe: 1.92 },
{ li: 844.60, ls: 7168.51, cf: 16.22, pe: 6.40 },
{ li: 7168.52, ls: 12598.02, cf: 420.95, pe: 10.88 },
{ li: 12598.03, ls: 14644.64, cf: 1011.68, pe: 16.00 },
{ li: 14644.65, ls: 17533.64, cf: 1339.14, pe: 17.92 },
{ li: 17533.65, ls: 35362.83, cf: 1856.84, pe: 21.36 },
{ li: 35362.84, ls: 55736.68, cf: 5665.16, pe: 23.52 },
{ li: 55736.69, ls: 106410.50, cf: 10457.09, pe: 30.00 },
{ li: 106410.51, ls: 141880.66, cf: 25659.23, pe: 32.00 },
{ li: 141880.67, ls: 425641.99, cf: 37009.69, pe: 34.00 },
{ li: 425642.00, ls: null, cf: 133488.54, pe: 35.00 },
],
};

View File

@@ -0,0 +1,634 @@
-- CreateEnum
CREATE TYPE "Plan" AS ENUM ('starter', 'business', 'business_ia', 'custom', 'enterprise');
-- CreateEnum
CREATE TYPE "PlatformRole" AS ENUM ('platform_admin', 'platform_ti', 'platform_support', 'platform_sales', 'platform_finance');
-- CreateEnum
CREATE TYPE "SatSyncType" AS ENUM ('initial', 'daily', 'incremental');
-- CreateEnum
CREATE TYPE "SatSyncStatus" AS ENUM ('pending', 'running', 'completed', 'failed');
-- CreateEnum
CREATE TYPE "CfdiSyncType" AS ENUM ('emitidos', 'recibidos');
-- CreateTable
CREATE TABLE "tenants" (
"id" TEXT NOT NULL,
"nombre" TEXT NOT NULL,
"rfc" TEXT NOT NULL,
"plan" "Plan" NOT NULL DEFAULT 'starter',
"database_name" TEXT NOT NULL,
"cfdi_limit" INTEGER NOT NULL DEFAULT 100,
"users_limit" INTEGER NOT NULL DEFAULT 1,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3),
"trial_ends_at" TIMESTAMP(3),
"facturapi_org_id" TEXT,
"codigo_postal" VARCHAR(5),
"calle" VARCHAR(255),
"num_exterior" VARCHAR(20),
"num_interior" VARCHAR(20),
"colonia" VARCHAR(255),
"ciudad" VARCHAR(100),
"municipio" VARCHAR(100),
"estado" VARCHAR(100),
"telefono" VARCHAR(20),
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"nombre" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"last_login" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"token_version" INTEGER NOT NULL DEFAULT 0,
"last_tenant_id" TEXT,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_memberships" (
"id" SERIAL NOT NULL,
"user_id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"rol_id" INTEGER NOT NULL,
"is_owner" BOOLEAN NOT NULL DEFAULT false,
"active" BOOLEAN NOT NULL DEFAULT true,
"joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_memberships_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "roles" (
"id" SERIAL NOT NULL,
"nombre" VARCHAR(20) NOT NULL,
"descripcion" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "refresh_tokens" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "password_reset_tokens" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"used_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "regimenes" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(3) NOT NULL,
"descripcion" TEXT NOT NULL,
"tipo_persona" VARCHAR(20) NOT NULL,
"activo" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "regimenes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_regimenes_ignorados" (
"id" SERIAL NOT NULL,
"tenant_id" TEXT NOT NULL,
"regimen_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_regimenes_ignorados_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_regimenes_activos" (
"id" SERIAL NOT NULL,
"tenant_id" TEXT NOT NULL,
"regimen_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_regimenes_activos_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "eventos_fiscales_catalogo" (
"id" SERIAL NOT NULL,
"titulo" TEXT NOT NULL,
"descripcion" TEXT,
"tipo" VARCHAR(20) NOT NULL,
"dia_base" INTEGER NOT NULL,
"mes_relativo" INTEGER NOT NULL DEFAULT 1,
"mes_fijo" INTEGER,
"recurrencia" VARCHAR(20) NOT NULL DEFAULT 'mensual',
"usa_extension_rfc" BOOLEAN NOT NULL DEFAULT false,
"regimenes" TEXT NOT NULL DEFAULT 'todos',
"condicion" VARCHAR(50),
"activo" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "eventos_fiscales_catalogo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lista_negra" (
"id" SERIAL NOT NULL,
"rfc" VARCHAR(13) NOT NULL,
"nombre" TEXT NOT NULL,
"situacion" VARCHAR(30) NOT NULL,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lista_negra_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "dias_inhabiles" (
"id" SERIAL NOT NULL,
"fecha" DATE NOT NULL,
"nombre" TEXT NOT NULL,
CONSTRAINT "dias_inhabiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "isr_resico_tasas" (
"id" SERIAL NOT NULL,
"anio" INTEGER NOT NULL,
"monto_maximo" DECIMAL(18,2) NOT NULL,
"porcentaje" DECIMAL(5,2) NOT NULL,
CONSTRAINT "isr_resico_tasas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "isr_tarifas" (
"id" SERIAL NOT NULL,
"anio" INTEGER NOT NULL,
"limite_inferior" DECIMAL(18,2) NOT NULL,
"limite_superior" DECIMAL(18,2),
"cuota_fija" DECIMAL(18,2) NOT NULL,
"porcentaje_excedente" DECIMAL(5,2) NOT NULL,
CONSTRAINT "isr_tarifas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "coeficiente_utilidad" (
"id" SERIAL NOT NULL,
"tenant_id" TEXT NOT NULL,
"anio" INTEGER NOT NULL,
"coeficiente" DECIMAL(10,4) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "coeficiente_utilidad_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "fiel_credentials" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"rfc" VARCHAR(13) NOT NULL,
"cer_data" BYTEA NOT NULL,
"key_data" BYTEA NOT NULL,
"key_password_encrypted" BYTEA NOT NULL,
"cer_iv" BYTEA NOT NULL,
"cer_tag" BYTEA NOT NULL,
"key_iv" BYTEA NOT NULL,
"key_tag" BYTEA NOT NULL,
"password_iv" BYTEA NOT NULL,
"password_tag" BYTEA NOT NULL,
"serial_number" VARCHAR(50),
"valid_from" TIMESTAMP(3) NOT NULL,
"valid_until" TIMESTAMP(3) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "fiel_credentials_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "subscriptions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"plan" "Plan" NOT NULL,
"mp_preapproval_id" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"amount" DECIMAL(10,2) NOT NULL,
"frequency" TEXT NOT NULL DEFAULT 'monthly',
"current_period_start" TIMESTAMP(3),
"current_period_end" TIMESTAMP(3),
"pending_plan" "Plan",
"pending_frequency" TEXT,
"pending_effective_at" TIMESTAMP(3),
"upgrade_preference_id" TEXT,
"upgrade_target_plan" "Plan",
"upgrade_target_amount" DECIMAL(10,2),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_platform_roles" (
"id" SERIAL NOT NULL,
"user_id" TEXT NOT NULL,
"role" "PlatformRole" NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT,
CONSTRAINT "user_platform_roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_log" (
"id" TEXT NOT NULL,
"user_id" TEXT,
"tenant_id" TEXT,
"action" VARCHAR(64) NOT NULL,
"entity_type" VARCHAR(32),
"entity_id" TEXT,
"metadata" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_log_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "trial_usages" (
"id" SERIAL NOT NULL,
"rfc" VARCHAR(13) NOT NULL,
"tenant_id" TEXT,
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "trial_usages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "plan_prices" (
"id" SERIAL NOT NULL,
"plan" "Plan" NOT NULL,
"frequency" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "plan_prices_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "payments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"subscription_id" TEXT,
"mp_payment_id" TEXT,
"amount" DECIMAL(10,2) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"payment_method" TEXT,
"paid_at" TIMESTAMP(3),
"facturapi_invoice_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sat_sync_jobs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"type" "SatSyncType" NOT NULL,
"status" "SatSyncStatus" NOT NULL DEFAULT 'pending',
"date_from" DATE NOT NULL,
"date_to" DATE NOT NULL,
"cfdi_type" "CfdiSyncType",
"sat_request_id" VARCHAR(50),
"sat_package_ids" TEXT[],
"cfdis_found" INTEGER NOT NULL DEFAULT 0,
"cfdis_downloaded" INTEGER NOT NULL DEFAULT 0,
"cfdis_inserted" INTEGER NOT NULL DEFAULT 0,
"cfdis_updated" INTEGER NOT NULL DEFAULT 0,
"progress_percent" INTEGER NOT NULL DEFAULT 0,
"error_message" TEXT,
"started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"retry_count" INTEGER NOT NULL DEFAULT 0,
"next_retry_at" TIMESTAMP(3),
CONSTRAINT "sat_sync_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_forma_pago" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(2) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_forma_pago_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_metodo_pago" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(3) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_metodo_pago_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_uso_cfdi" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(4) NOT NULL,
"descripcion" TEXT NOT NULL,
"persona_fisica" BOOLEAN NOT NULL DEFAULT true,
"persona_moral" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "cat_uso_cfdi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_moneda" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(3) NOT NULL,
"descripcion" TEXT NOT NULL,
"decimales" INTEGER NOT NULL DEFAULT 2,
CONSTRAINT "cat_moneda_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_clave_unidad" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(10) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_clave_unidad_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_clave_prod_serv" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(8) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_clave_prod_serv_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_objeto_imp" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(2) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_objeto_imp_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_tipo_relacion" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(2) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_tipo_relacion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cat_exportacion" (
"id" SERIAL NOT NULL,
"clave" VARCHAR(2) NOT NULL,
"descripcion" TEXT NOT NULL,
CONSTRAINT "cat_exportacion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "timbre_suscripciones" (
"id" SERIAL NOT NULL,
"tenant_id" TEXT NOT NULL,
"tipo" VARCHAR(10) NOT NULL,
"timbres_limite" INTEGER NOT NULL,
"timbres_usados" INTEGER NOT NULL DEFAULT 0,
"periodo_inicio" DATE NOT NULL,
"periodo_fin" DATE NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "timbre_suscripciones_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tenants_rfc_key" ON "tenants"("rfc");
-- CreateIndex
CREATE UNIQUE INDEX "tenants_database_name_key" ON "tenants"("database_name");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "tenant_memberships_user_id_active_idx" ON "tenant_memberships"("user_id", "active");
-- CreateIndex
CREATE INDEX "tenant_memberships_tenant_id_active_idx" ON "tenant_memberships"("tenant_id", "active");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_memberships_user_id_tenant_id_key" ON "tenant_memberships"("user_id", "tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "roles_nombre_key" ON "roles"("nombre");
-- CreateIndex
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
-- CreateIndex
CREATE UNIQUE INDEX "password_reset_tokens_token_key" ON "password_reset_tokens"("token");
-- CreateIndex
CREATE INDEX "password_reset_tokens_user_id_idx" ON "password_reset_tokens"("user_id");
-- CreateIndex
CREATE INDEX "password_reset_tokens_expires_at_idx" ON "password_reset_tokens"("expires_at");
-- CreateIndex
CREATE UNIQUE INDEX "regimenes_clave_key" ON "regimenes"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_regimenes_ignorados_tenant_id_regimen_id_key" ON "tenant_regimenes_ignorados"("tenant_id", "regimen_id");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_regimenes_activos_tenant_id_regimen_id_key" ON "tenant_regimenes_activos"("tenant_id", "regimen_id");
-- CreateIndex
CREATE UNIQUE INDEX "lista_negra_rfc_key" ON "lista_negra"("rfc");
-- CreateIndex
CREATE INDEX "lista_negra_rfc_idx" ON "lista_negra"("rfc");
-- CreateIndex
CREATE UNIQUE INDEX "dias_inhabiles_fecha_key" ON "dias_inhabiles"("fecha");
-- CreateIndex
CREATE UNIQUE INDEX "isr_resico_tasas_anio_monto_maximo_key" ON "isr_resico_tasas"("anio", "monto_maximo");
-- CreateIndex
CREATE UNIQUE INDEX "isr_tarifas_anio_limite_inferior_key" ON "isr_tarifas"("anio", "limite_inferior");
-- CreateIndex
CREATE UNIQUE INDEX "coeficiente_utilidad_tenant_id_anio_key" ON "coeficiente_utilidad"("tenant_id", "anio");
-- CreateIndex
CREATE UNIQUE INDEX "fiel_credentials_tenant_id_key" ON "fiel_credentials"("tenant_id");
-- CreateIndex
CREATE INDEX "subscriptions_tenant_id_idx" ON "subscriptions"("tenant_id");
-- CreateIndex
CREATE INDEX "subscriptions_status_idx" ON "subscriptions"("status");
-- CreateIndex
CREATE INDEX "subscriptions_pending_effective_at_idx" ON "subscriptions"("pending_effective_at");
-- CreateIndex
CREATE INDEX "user_platform_roles_role_idx" ON "user_platform_roles"("role");
-- CreateIndex
CREATE UNIQUE INDEX "user_platform_roles_user_id_role_key" ON "user_platform_roles"("user_id", "role");
-- CreateIndex
CREATE INDEX "audit_log_user_id_created_at_idx" ON "audit_log"("user_id", "created_at");
-- CreateIndex
CREATE INDEX "audit_log_tenant_id_created_at_idx" ON "audit_log"("tenant_id", "created_at");
-- CreateIndex
CREATE INDEX "audit_log_action_created_at_idx" ON "audit_log"("action", "created_at");
-- CreateIndex
CREATE INDEX "audit_log_entity_type_entity_id_idx" ON "audit_log"("entity_type", "entity_id");
-- CreateIndex
CREATE UNIQUE INDEX "trial_usages_rfc_key" ON "trial_usages"("rfc");
-- CreateIndex
CREATE UNIQUE INDEX "plan_prices_plan_frequency_key" ON "plan_prices"("plan", "frequency");
-- CreateIndex
CREATE INDEX "payments_tenant_id_idx" ON "payments"("tenant_id");
-- CreateIndex
CREATE INDEX "payments_subscription_id_idx" ON "payments"("subscription_id");
-- CreateIndex
CREATE INDEX "sat_sync_jobs_tenant_id_idx" ON "sat_sync_jobs"("tenant_id");
-- CreateIndex
CREATE INDEX "sat_sync_jobs_status_idx" ON "sat_sync_jobs"("status");
-- CreateIndex
CREATE INDEX "sat_sync_jobs_status_next_retry_at_idx" ON "sat_sync_jobs"("status", "next_retry_at");
-- CreateIndex
CREATE UNIQUE INDEX "cat_forma_pago_clave_key" ON "cat_forma_pago"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_metodo_pago_clave_key" ON "cat_metodo_pago"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_uso_cfdi_clave_key" ON "cat_uso_cfdi"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_moneda_clave_key" ON "cat_moneda"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_clave_unidad_clave_key" ON "cat_clave_unidad"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_clave_prod_serv_clave_key" ON "cat_clave_prod_serv"("clave");
-- CreateIndex
CREATE INDEX "cat_clave_prod_serv_descripcion_idx" ON "cat_clave_prod_serv"("descripcion");
-- CreateIndex
CREATE UNIQUE INDEX "cat_objeto_imp_clave_key" ON "cat_objeto_imp"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_tipo_relacion_clave_key" ON "cat_tipo_relacion"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "cat_exportacion_clave_key" ON "cat_exportacion"("clave");
-- CreateIndex
CREATE UNIQUE INDEX "timbre_suscripciones_tenant_id_key" ON "timbre_suscripciones"("tenant_id");
-- AddForeignKey
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_rol_id_fkey" FOREIGN KEY ("rol_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_regimenes_ignorados" ADD CONSTRAINT "tenant_regimenes_ignorados_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_regimenes_ignorados" ADD CONSTRAINT "tenant_regimenes_ignorados_regimen_id_fkey" FOREIGN KEY ("regimen_id") REFERENCES "regimenes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_regimenes_activos" ADD CONSTRAINT "tenant_regimenes_activos_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_regimenes_activos" ADD CONSTRAINT "tenant_regimenes_activos_regimen_id_fkey" FOREIGN KEY ("regimen_id") REFERENCES "regimenes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "coeficiente_utilidad" ADD CONSTRAINT "coeficiente_utilidad_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "fiel_credentials" ADD CONSTRAINT "fiel_credentials_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_platform_roles" ADD CONSTRAINT "user_platform_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sat_sync_jobs" ADD CONSTRAINT "sat_sync_jobs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "timbre_suscripciones" ADD CONSTRAINT "timbre_suscripciones_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,47 @@
-- CreateEnum
CREATE TYPE "PaymentKind" AS ENUM ('subscription', 'timbres_pack');
-- AlterTable
ALTER TABLE "payments" ADD COLUMN "kind" "PaymentKind" NOT NULL DEFAULT 'subscription';
-- CreateTable
CREATE TABLE "timbre_paquetes_catalogo" (
"id" SERIAL NOT NULL,
"cantidad" INTEGER NOT NULL,
"precio" DECIMAL(10,2) NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "timbre_paquetes_catalogo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "timbre_paquetes" (
"id" SERIAL NOT NULL,
"tenant_id" TEXT NOT NULL,
"payment_id" TEXT,
"cantidad" INTEGER NOT NULL,
"usados" INTEGER NOT NULL DEFAULT 0,
"precio" DECIMAL(10,2) NOT NULL,
"adquirido_en" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expira_en" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "timbre_paquetes_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "timbre_paquetes_catalogo_cantidad_key" ON "timbre_paquetes_catalogo"("cantidad");
-- CreateIndex
CREATE UNIQUE INDEX "timbre_paquetes_payment_id_key" ON "timbre_paquetes"("payment_id");
-- CreateIndex
CREATE INDEX "timbre_paquetes_tenant_id_expira_en_idx" ON "timbre_paquetes"("tenant_id", "expira_en");
-- AddForeignKey
ALTER TABLE "timbre_paquetes" ADD CONSTRAINT "timbre_paquetes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "timbre_paquetes" ADD CONSTRAINT "timbre_paquetes_payment_id_fkey" FOREIGN KEY ("payment_id") REFERENCES "payments"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,16 @@
-- CreateEnum
CREATE TYPE "VerticalProfile" AS ENUM ('CONTABLE', 'JURIDICO', 'ARQUITECTURA');
-- CreateEnum
CREATE TYPE "DbMode" AS ENUM ('BYO', 'MANAGED');
-- AlterTable
ALTER TABLE "tenants" ADD COLUMN "connector_last_seen" TIMESTAMP(3),
ADD COLUMN "connector_token_enc" TEXT,
ADD COLUMN "connector_tunnel_hostname" TEXT,
ADD COLUMN "connector_version" VARCHAR(20),
ADD COLUMN "db_connection_enc" TEXT,
ADD COLUMN "db_connection_iv" TEXT,
ADD COLUMN "db_mode" "DbMode",
ADD COLUMN "db_schema_version" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "vertical_profile" "VerticalProfile";

View File

@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "plan_catalogo" (
"id" TEXT NOT NULL,
"codename" VARCHAR(50) NOT NULL,
"nombre" TEXT NOT NULL,
"verticalProfile" "VerticalProfile" NOT NULL,
"precio_base" DECIMAL(10,2) NOT NULL,
"frecuencia" VARCHAR(10) NOT NULL,
"limits" JSONB NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "plan_catalogo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "plan_addon_catalogo" (
"id" TEXT NOT NULL,
"codename" VARCHAR(50) NOT NULL,
"nombre" TEXT NOT NULL,
"verticalProfile" "VerticalProfile",
"precio" DECIMAL(10,2) NOT NULL,
"frecuencia" VARCHAR(10) NOT NULL,
"delta" JSONB NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "plan_addon_catalogo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "plan_catalogo_codename_key" ON "plan_catalogo"("codename");
-- CreateIndex
CREATE UNIQUE INDEX "plan_addon_catalogo_codename_key" ON "plan_addon_catalogo"("codename");

View File

@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "subscription_addons" (
"id" TEXT NOT NULL,
"subscription_id" TEXT NOT NULL,
"plan_addon_catalogo_id" TEXT NOT NULL,
"mp_preapproval_id" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"quantity" INTEGER NOT NULL DEFAULT 1,
"amount" DECIMAL(10,2) NOT NULL,
"current_period_start" TIMESTAMP(3),
"current_period_end" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "subscription_addons_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "subscription_addons_subscription_id_idx" ON "subscription_addons"("subscription_id");
-- CreateIndex
CREATE UNIQUE INDEX "subscription_addons_subscription_id_plan_addon_catalogo_id_key" ON "subscription_addons"("subscription_id", "plan_addon_catalogo_id");
-- AddForeignKey
ALTER TABLE "subscription_addons" ADD CONSTRAINT "subscription_addons_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "subscriptions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subscription_addons" ADD CONSTRAINT "subscription_addons_plan_addon_catalogo_id_fkey" FOREIGN KEY ("plan_addon_catalogo_id") REFERENCES "plan_addon_catalogo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "connector_heartbeats" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"latency_ms" INTEGER NOT NULL,
"version" VARCHAR(20) NOT NULL,
"pg_version" VARCHAR(50),
"status" VARCHAR(20) NOT NULL,
"error_msg" TEXT,
CONSTRAINT "connector_heartbeats_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "connector_heartbeats_tenant_id_timestamp_idx" ON "connector_heartbeats"("tenant_id", "timestamp");
-- AddForeignKey
ALTER TABLE "connector_heartbeats" ADD CONSTRAINT "connector_heartbeats_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "sat_sync_jobs" ADD COLUMN "contribuyente_id" TEXT;

View File

@@ -0,0 +1,10 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "Plan" ADD VALUE 'business_control';
ALTER TYPE "Plan" ADD VALUE 'business_cloud';

View File

@@ -0,0 +1,18 @@
-- Add-ons por contribuyente: permite que SubscriptionAddon se asocie a un
-- contribuyente específico (ej. Lolita IA $250/mes activable por RFC) además
-- de los add-ons a nivel tenant (modulos, +RFCs, +timbres) que tienen
-- contribuyente_id = NULL.
ALTER TABLE "subscription_addons"
ADD COLUMN "contribuyente_id" TEXT;
-- Eliminar el UNIQUE (subscription_id, plan_addon_catalogo_id). Ahora el
-- mismo add-on (p. ej. lolita_ia_contribuyente) puede tener N filas por
-- subscription, una por cada contribuyente que lo contrate.
ALTER TABLE "subscription_addons"
DROP CONSTRAINT IF EXISTS "subscription_addons_subscription_id_plan_addon_catalogo_id_key";
-- Índice por (subscription_id, contribuyente_id) para lookups rápidos
-- "qué add-ons tiene este contribuyente"
CREATE INDEX IF NOT EXISTS "subscription_addons_subscription_id_contribuyente_id_idx"
ON "subscription_addons"("subscription_id", "contribuyente_id");

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Plan" ADD VALUE 'mi_empresa';

View File

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "despacho_plan_prices" (
"plan" TEXT NOT NULL,
"monthly" DECIMAL(10,2),
"first_year" DECIMAL(10,2) NOT NULL,
"renewal" DECIMAL(10,2) NOT NULL,
"permite_monthly" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "despacho_plan_prices_pkey" PRIMARY KEY ("plan")
);
-- Seed inicial con valores actuales del catálogo `DESPACHO_PLAN_PRICES`.
INSERT INTO "despacho_plan_prices" ("plan", "monthly", "first_year", "renewal", "permite_monthly", "updated_at") VALUES
('mi_empresa', 580, 5800, 5800, true, NOW()),
('mi_empresa_plus', 900, 9000, 9000, true, NOW()),
('business_control', NULL, 25850, 25850, false, NOW()),
('business_cloud', NULL, 43000, 43000, false, NOW());

View File

@@ -0,0 +1,19 @@
-- AlterEnum
BEGIN;
CREATE TYPE "Plan_new" AS ENUM ('trial', 'custom', 'business_control', 'business_cloud', 'mi_empresa', 'mi_empresa_plus');
ALTER TABLE "tenants" ALTER COLUMN "plan" DROP DEFAULT;
ALTER TABLE "tenants" ALTER COLUMN "plan" TYPE "Plan_new" USING ("plan"::text::"Plan_new");
ALTER TABLE "subscriptions" ALTER COLUMN "plan" TYPE "Plan_new" USING ("plan"::text::"Plan_new");
ALTER TABLE "subscriptions" ALTER COLUMN "pending_plan" TYPE "Plan_new" USING ("pending_plan"::text::"Plan_new");
ALTER TABLE "subscriptions" ALTER COLUMN "upgrade_target_plan" TYPE "Plan_new" USING ("upgrade_target_plan"::text::"Plan_new");
ALTER TABLE "plan_prices" ALTER COLUMN "plan" TYPE "Plan_new" USING ("plan"::text::"Plan_new");
ALTER TYPE "Plan" RENAME TO "Plan_old";
ALTER TYPE "Plan_new" RENAME TO "Plan";
DROP TYPE "Plan_old";
ALTER TABLE "tenants" ALTER COLUMN "plan" SET DEFAULT 'trial';
COMMIT;
-- AlterTable
ALTER TABLE "tenants" DROP COLUMN "cfdi_limit",
DROP COLUMN "users_limit",
ALTER COLUMN "plan" SET DEFAULT 'trial';

View File

@@ -0,0 +1,55 @@
-- Step 1: Add new columns as nullable (preserva las 4 filas existentes con sus precios)
ALTER TABLE "despacho_plan_prices"
ADD COLUMN "nombre" VARCHAR(50),
ADD COLUMN "max_rfcs" INTEGER,
ADD COLUMN "max_users" INTEGER,
ADD COLUMN "db_mode" "DbMode",
ADD COLUMN "timbres_incluidos_mes" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "permite_servidor_backup" BOOLEAN NOT NULL DEFAULT false;
-- Step 2: Backfill limits para las 4 filas existentes desde el catálogo TS
UPDATE "despacho_plan_prices" SET
"nombre" = 'Mi Empresa',
"max_rfcs" = 1,
"max_users" = 3,
"timbres_incluidos_mes" = 50,
"db_mode" = 'MANAGED'
WHERE "plan" = 'mi_empresa';
UPDATE "despacho_plan_prices" SET
"nombre" = 'Mi Empresa +',
"max_rfcs" = 1,
"max_users" = 3,
"timbres_incluidos_mes" = 50,
"db_mode" = 'MANAGED'
WHERE "plan" = 'mi_empresa_plus';
UPDATE "despacho_plan_prices" SET
"nombre" = 'Business Control',
"max_rfcs" = 100,
"max_users" = -1,
"timbres_incluidos_mes" = 0,
"db_mode" = 'BYO',
"permite_servidor_backup" = true
WHERE "plan" = 'business_control';
UPDATE "despacho_plan_prices" SET
"nombre" = 'Enterprise',
"max_rfcs" = 100,
"max_users" = -1,
"timbres_incluidos_mes" = 0,
"db_mode" = 'BYO',
"permite_servidor_backup" = true
WHERE "plan" = 'business_cloud';
-- Step 3: Set NOT NULL después del backfill (las 4 filas ya están completas)
ALTER TABLE "despacho_plan_prices"
ALTER COLUMN "nombre" SET NOT NULL,
ALTER COLUMN "max_rfcs" SET NOT NULL,
ALTER COLUMN "max_users" SET NOT NULL,
ALTER COLUMN "db_mode" SET NOT NULL;
-- Step 4: Hacer firstYear y renewal nullable para soportar trial y custom (sin precio fijo)
ALTER TABLE "despacho_plan_prices"
ALTER COLUMN "first_year" DROP NOT NULL,
ALTER COLUMN "renewal" DROP NOT NULL;

View File

@@ -0,0 +1,5 @@
-- Drop tabla plan_catalogo (modelo huérfano nunca usado por código activo).
-- Las 2 filas que tenía estaban desincronizadas con el catálogo TS y nunca
-- se referenciaron desde código real. El catálogo despacho vive ahora en
-- `despacho_plan_prices` (extendida con limits en migración 20260430195000).
DROP TABLE "plan_catalogo";

View File

@@ -0,0 +1,10 @@
-- Add column with default false (no-op para filas existentes)
ALTER TABLE "despacho_plan_prices"
ADD COLUMN "permite_sat_incremental" BOOLEAN NOT NULL DEFAULT false;
-- Backfill: planes que SÍ deben tener incremental (3 syncs/día adicionales).
-- Mi Empresa + tiene API + Lolita IA y precio premium ($9k anual);
-- Business Control y Enterprise son los planes despacho con escala alta.
UPDATE "despacho_plan_prices"
SET "permite_sat_incremental" = true
WHERE "plan" IN ('mi_empresa_plus', 'business_control', 'business_cloud');

View File

@@ -0,0 +1,7 @@
-- Cache cifrada de la Live Secret Key de la organización Facturapi del tenant
-- central (Horux 360 admin que emite facturas de subscripción a clientes).
-- AES-256-GCM con derivación FIEL_ENCRYPTION_KEY — mismo patrón que FIEL.
ALTER TABLE "tenants"
ADD COLUMN "facturapi_org_key_enc" BYTEA,
ADD COLUMN "facturapi_org_key_iv" BYTEA,
ADD COLUMN "facturapi_org_key_tag" BYTEA;

View File

@@ -0,0 +1,5 @@
-- Drop tabla plan_prices (modelo legacy Horux 360 sin filas activas).
-- Catálogo se reemplazó por DespachoPlanPrice (despacho_plan_prices) en
-- migración 20260430195000_extend_despacho_plan_prices_with_limits.
-- Sin callers activos en código (verificado vía typecheck post-cleanup).
DROP TABLE "plan_prices";

View File

@@ -0,0 +1,5 @@
-- Tracking de aviso pre-vencimiento por suscripción. Permite que el cron diario
-- evite enviar dos emails del mismo bucket de días al mismo owner.
ALTER TABLE "subscriptions"
ADD COLUMN "last_reminder_day" INTEGER,
ADD COLUMN "last_reminder_sent_at" TIMESTAMP(3);

View File

@@ -0,0 +1,8 @@
-- Preferencias de auto-facturación de pagos de suscripción.
-- factPreferencia: 'publico_general' o 'mis_datos' (default: mis_datos)
-- factUsoCfdi: clave SAT del uso CFDI default (G03 = Gastos en general)
-- factRegimenPreferido: clave del régimen fiscal a usar cuando hay multi-régimen
ALTER TABLE "tenants"
ADD COLUMN "fact_preferencia" VARCHAR(20) DEFAULT 'mis_datos' NOT NULL,
ADD COLUMN "fact_uso_cfdi" VARCHAR(5) DEFAULT 'G03' NOT NULL,
ADD COLUMN "fact_regimen_preferido" VARCHAR(3);

View File

@@ -0,0 +1,4 @@
-- Onboarding auto-dismiss: 4 logins ó pasos completados, lo que pase primero.
ALTER TABLE "users"
ADD COLUMN "login_count" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "onboarding_dismissed_at" TIMESTAMP(3);

View File

@@ -0,0 +1,6 @@
-- Mapa { kindKey: requestId } para reusar requests del SAT en reintentos.
-- Hasta antes de este cambio, cada retry creaba nuevas solicitudes — agotaba
-- la cuota del SAT y abandonaba requests anteriores. Ahora el retry consulta
-- los requestIds previos antes de crear nuevos.
ALTER TABLE "sat_sync_jobs"
ADD COLUMN "sat_request_ids" JSONB NOT NULL DEFAULT '{}'::jsonb;

View File

@@ -0,0 +1,6 @@
-- Distingue extracciones tipo `initial` con rango personalizado (UI custom)
-- de bootstrap inicial puro. Política de retry distinta:
-- initial bootstrap → 3 retries a 6h, 12h, 24h
-- initial custom → 2 retries a 6h, 12h
ALTER TABLE "sat_sync_jobs"
ADD COLUMN "is_custom_range" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -0,0 +1,760 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Tenant {
id String @id @default(uuid())
nombre String
rfc String @unique
plan Plan @default(trial)
databaseName String @unique @map("database_name")
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime? @map("expires_at")
// Prueba gratuita: si está set y en el futuro, el tenant está en trial.
// Se consume una sola vez por tenant (al activarla, nunca se regenera).
trialEndsAt DateTime? @map("trial_ends_at")
facturapiOrgId String? @map("facturapi_org_id")
/// Live Secret Key cifrada (AES-256-GCM, misma derivación FIEL_ENCRYPTION_KEY).
/// Cacheada tras primer PUT idempotente a /v2/organizations/{id}/apikeys/live.
facturapiOrgKeyEnc Bytes? @map("facturapi_org_key_enc")
facturapiOrgKeyIv Bytes? @map("facturapi_org_key_iv")
facturapiOrgKeyTag Bytes? @map("facturapi_org_key_tag")
// Domicilio fiscal
codigoPostal String? @map("codigo_postal") @db.VarChar(5)
calle String? @db.VarChar(255)
numExterior String? @map("num_exterior") @db.VarChar(20)
numInterior String? @map("num_interior") @db.VarChar(20)
colonia String? @db.VarChar(255)
ciudad String? @db.VarChar(100)
municipio String? @db.VarChar(100)
estado String? @db.VarChar(100)
telefono String? @db.VarChar(20)
// Preferencias de auto-facturación de pagos de suscripción.
// Default: facturar con datos del cliente cuando hay CSF disponible.
// Si `factPreferencia='publico_general'` siempre va a XAXX010101000.
factPreferencia String @default("mis_datos") @map("fact_preferencia") @db.VarChar(20)
// Uso CFDI default cuando se factura con datos del cliente.
// G03 = Gastos en general (más común para SaaS).
factUsoCfdi String @default("G03") @map("fact_uso_cfdi") @db.VarChar(5)
// Si el tenant tiene múltiples regímenes activos, cuál usar para factura.
// Null = usar el primero activo (heurística por createdAt).
factRegimenPreferido String? @map("fact_regimen_preferido") @db.VarChar(3)
// === Despacho fields ===
verticalProfile VerticalProfile? @map("vertical_profile")
dbMode DbMode? @map("db_mode")
dbConnectionEnc String? @map("db_connection_enc")
dbConnectionIv String? @map("db_connection_iv")
dbSchemaVersion Int @default(0) @map("db_schema_version")
connectorTokenEnc String? @map("connector_token_enc")
connectorTunnelHostname String? @map("connector_tunnel_hostname")
connectorLastSeen DateTime? @map("connector_last_seen")
connectorVersion String? @map("connector_version") @db.VarChar(20)
memberships TenantMembership[]
fielCredential FielCredential?
satSyncJobs SatSyncJob[]
subscriptions Subscription[]
payments Payment[]
regimenesIgnorados TenantRegimenIgnorado[]
regimenesActivos TenantRegimenActivo[]
coeficientes CoeficienteUtilidad[]
timbreSuscripcion TimbreSuscripcion?
timbrePaquetes TimbrePaquete[]
connectorHeartbeats ConnectorHeartbeat[]
@@map("tenants")
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String @map("password_hash")
nombre String
active Boolean @default(true)
lastLogin DateTime? @map("last_login")
createdAt DateTime @default(now()) @map("created_at")
// Contador para invalidar sesiones masivamente. Al incrementar, todos los
// JWT emitidos antes (con tokenVersion menor) quedan rechazados en el
// siguiente request. Se incrementa en: password change, password reset,
// logout-all. Default 0 para compat con users pre-rollout.
tokenVersion Int @default(0) @map("token_version")
// Último tenant que el user activó (via switch-tenant). Se usa para resolver
// el "tenant activo al login". Si es null, el login cae al primer membership
// por joinedAt. Se actualiza en cada switch.
lastTenantId String? @map("last_tenant_id")
// Cuenta sesiones (login exitoso, NO refresh). Usado para auto-dismiss del
// onboarding tras N logins. Default 0 → users pre-rollout siguen viendo el
// onboarding hasta acumular logins post-deploy.
loginCount Int @default(0) @map("login_count")
// Marca explícita de que el onboarding ya no debe mostrarse. Se setea cuando
// el user completa todos los pasos requeridos o desde el endpoint de dismiss.
onboardingDismissedAt DateTime? @map("onboarding_dismissed_at")
memberships TenantMembership[]
platformRoles UserPlatformRole[]
passwordResetTokens PasswordResetToken[]
@@map("users")
}
/// Relación many-to-many entre User y Tenant. Permite que un mismo user (p.ej.
/// un dueño/contador) pertenezca a varios tenants con distintos roles. Esta
/// tabla es la fuente de verdad del "¿a qué tenants tiene acceso este user?".
///
/// Durante la transición, `User.tenantId` y `User.rolId` se mantienen como
/// "default tenant" para login UX. El backfill inicial crea 1 membership por
/// user basado en esos campos. Cuando se agregue la UI de multi-tenant, los
/// nuevos accesos solo tocarán esta tabla.
model TenantMembership {
id Int @id @default(autoincrement())
userId String @map("user_id")
tenantId String @map("tenant_id")
rolId Int @map("rol_id")
isOwner Boolean @default(false) @map("is_owner")
active Boolean @default(true)
joinedAt DateTime @default(now()) @map("joined_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
rol Rol @relation(fields: [rolId], references: [id])
@@unique([userId, tenantId])
@@index([userId, active])
@@index([tenantId, active])
@@map("tenant_memberships")
}
model Rol {
id Int @id @default(autoincrement())
nombre String @unique @db.VarChar(20)
descripcion String?
createdAt DateTime @default(now()) @map("created_at")
memberships TenantMembership[]
@@map("roles")
}
model RefreshToken {
id String @id @default(uuid())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
@@map("refresh_tokens")
}
/// Tokens para recuperación de contraseña. Expiran en 1 hora, son single-use
/// (se marca `usedAt` al consumir). Al completar reset se invalidan todos los
/// refresh tokens del user — cierra todas sus sesiones forzando re-login.
model PasswordResetToken {
id String @id @default(uuid())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
usedAt DateTime? @map("used_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
@@map("password_reset_tokens")
}
enum Plan {
trial
custom
business_control
business_cloud
mi_empresa
mi_empresa_plus
}
enum VerticalProfile {
CONTABLE
JURIDICO
ARQUITECTURA
}
enum DbMode {
BYO
MANAGED
}
// ============================================
// Catálogo de Regímenes Fiscales SAT
// ============================================
model Regimen {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(3)
descripcion String
tipoPersona String @map("tipo_persona") @db.VarChar(20) // fisica, moral, ambos
activo Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
tenantIgnorados TenantRegimenIgnorado[]
tenantActivos TenantRegimenActivo[]
@@map("regimenes")
}
model TenantRegimenIgnorado {
id Int @id @default(autoincrement())
tenantId String @map("tenant_id")
regimenId Int @map("regimen_id")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
regimen Regimen @relation(fields: [regimenId], references: [id], onDelete: Cascade)
@@unique([tenantId, regimenId])
@@map("tenant_regimenes_ignorados")
}
model TenantRegimenActivo {
id Int @id @default(autoincrement())
tenantId String @map("tenant_id")
regimenId Int @map("regimen_id")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
regimen Regimen @relation(fields: [regimenId], references: [id], onDelete: Cascade)
@@unique([tenantId, regimenId])
@@map("tenant_regimenes_activos")
}
// ============================================
// Catálogo de Eventos Fiscales
// ============================================
model EventoFiscalCatalogo {
id Int @id @default(autoincrement())
titulo String
descripcion String?
tipo String @db.VarChar(20) // declaracion, pago, obligacion, informativa
diaBase Int @map("dia_base") // día del mes (17, 3, 31, etc.)
mesRelativo Int @default(1) @map("mes_relativo") // 1=mes posterior, 2=segundo mes posterior, 0=mes fijo
mesFijo Int? @map("mes_fijo") // para anuales: 2=feb, 3=mar, 4=abr
recurrencia String @default("mensual") @db.VarChar(20) // mensual, anual
usaExtensionRfc Boolean @default(false) @map("usa_extension_rfc")
regimenes String @default("todos") // 'todos' o CSV de claves: '601,603,612'
condicion String? @db.VarChar(50) // null, 'tiene_nomina', 'ingresos_4m'
activo Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
@@map("eventos_fiscales_catalogo")
}
/// Lista negra SAT (Art. 69-B CFF)
model ListaNegra {
id Int @id @default(autoincrement())
rfc String @unique @db.VarChar(13)
nombre String
situacion String @db.VarChar(30) // Definitivo, Presunto, Desvirtuado, Sentencia Favorable
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
@@index([rfc])
@@map("lista_negra")
}
/// Días inhábiles fiscales (festivos oficiales de México)
model DiaInhabil {
id Int @id @default(autoincrement())
fecha DateTime @unique @db.Date
nombre String
@@map("dias_inhabiles")
}
// ============================================
// ISR Tables
// ============================================
/// Tasas RESICO (Art. 113-E) - tasa plana por bracket mensual
model IsrResicoTasa {
id Int @id @default(autoincrement())
anio Int @map("anio")
montoMaximo Decimal @map("monto_maximo") @db.Decimal(18, 2)
porcentaje Decimal @db.Decimal(5, 2)
@@unique([anio, montoMaximo])
@@map("isr_resico_tasas")
}
/// Tarifa ISR progresiva (Art. 96) - mensual
model IsrTarifa {
id Int @id @default(autoincrement())
anio Int @map("anio")
limiteInferior Decimal @map("limite_inferior") @db.Decimal(18, 2)
limiteSuperior Decimal? @map("limite_superior") @db.Decimal(18, 2)
cuotaFija Decimal @map("cuota_fija") @db.Decimal(18, 2)
porcentajeExcedente Decimal @map("porcentaje_excedente") @db.Decimal(5, 2)
@@unique([anio, limiteInferior])
@@map("isr_tarifas")
}
/// Coeficiente de utilidad por tenant/año (no se sobrescribe)
model CoeficienteUtilidad {
id Int @id @default(autoincrement())
tenantId String @map("tenant_id")
anio Int @map("anio")
coeficiente Decimal @db.Decimal(10, 4)
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@unique([tenantId, anio])
@@map("coeficiente_utilidad")
}
// ============================================
// SAT Sync Models
// ============================================
model FielCredential {
id String @id @default(uuid())
tenantId String @unique @map("tenant_id")
rfc String @db.VarChar(13)
cerData Bytes @map("cer_data")
keyData Bytes @map("key_data")
keyPasswordEncrypted Bytes @map("key_password_encrypted")
cerIv Bytes @map("cer_iv")
cerTag Bytes @map("cer_tag")
keyIv Bytes @map("key_iv")
keyTag Bytes @map("key_tag")
passwordIv Bytes @map("password_iv")
passwordTag Bytes @map("password_tag")
serialNumber String? @map("serial_number") @db.VarChar(50)
validFrom DateTime @map("valid_from")
validUntil DateTime @map("valid_until")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@map("fiel_credentials")
}
model Subscription {
id String @id @default(uuid())
tenantId String @map("tenant_id")
plan Plan
mpPreapprovalId String? @map("mp_preapproval_id")
status String @default("pending")
amount Decimal @db.Decimal(10, 2)
frequency String @default("monthly")
currentPeriodStart DateTime? @map("current_period_start")
currentPeriodEnd DateTime? @map("current_period_end")
// Cambio programado al próximo período (downgrades y cambios de frecuencia)
pendingPlan Plan? @map("pending_plan")
pendingFrequency String? @map("pending_frequency")
pendingEffectiveAt DateTime? @map("pending_effective_at")
// Upgrade inmediato en curso: preference MP esperando cobro prorateado.
// Cuando el webhook confirma el pago, se aplica el plan nuevo y se limpian estos campos.
upgradePreferenceId String? @map("upgrade_preference_id")
upgradeTargetPlan Plan? @map("upgrade_target_plan")
upgradeTargetAmount Decimal? @db.Decimal(10, 2) @map("upgrade_target_amount")
// Idempotencia del cron de aviso pre-vencimiento. Guarda el bucket de días
// que ya se notificó (7, 3, 1 ó 0) para no spamear al owner si el cron corre
// dos veces el mismo día. Se resetea cuando se renueva la suscripción o
// arranca un período nuevo.
lastReminderDay Int? @map("last_reminder_day")
lastReminderSentAt DateTime? @map("last_reminder_sent_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
payments Payment[]
addons SubscriptionAddon[]
@@index([tenantId])
@@index([status])
@@index([pendingEffectiveAt])
@@map("subscriptions")
}
model SubscriptionAddon {
id String @id @default(uuid())
subscriptionId String @map("subscription_id")
planAddonCatalogoId String @map("plan_addon_catalogo_id")
/// UUID del contribuyente (entidad_id en tenant BD) cuando el add-on
/// aplica a un RFC específico. NULL para add-ons a nivel tenant (módulos
/// globales, +RFCs, +timbres). Sin FK porque contribuyente vive en BD tenant.
contribuyenteId String? @map("contribuyente_id")
mpPreapprovalId String? @map("mp_preapproval_id")
status String @default("pending")
quantity Int @default(1)
amount Decimal @db.Decimal(10, 2)
currentPeriodStart DateTime? @map("current_period_start")
currentPeriodEnd DateTime? @map("current_period_end")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
subscription Subscription @relation(fields: [subscriptionId], references: [id])
planAddonCatalogo PlanAddonCatalogo @relation(fields: [planAddonCatalogoId], references: [id])
/// Sin UNIQUE compuesto: la validación de "un solo add-on activo por
/// (subscription, addon, contribuyente?)" queda a nivel aplicación
/// (findFirst en subscribeAddon), porque Postgres trata NULL!=NULL y no
/// hay forma trivial de enforcar unicidad con contribuyenteId opcional.
@@index([subscriptionId])
@@index([subscriptionId, contribuyenteId])
@@map("subscription_addons")
}
/// Roles de plataforma (staff interno de Horux 360) — ortogonales al rol per-tenant.
/// Un user puede tener 0, 1 o varios roles. `platform_admin` es el superrol.
/// Ver `docs/plans/2026-04-14-platform-admin-roles.md`.
enum PlatformRole {
platform_admin // Todo: precios, clientes, facturas, suscripciones, gestión de staff
platform_ti // Mismos permisos que admin (equipo de TI / tech ops). Diferencia solo en trazabilidad.
platform_support // Ver todos los tenants, resolver tickets, NO facturación/precios
platform_sales // Crear/editar tenants (onboarding), ver suscripciones, NO precios
platform_finance // Ver payments, emitir facturas manuales, editar precios, reportes fiscales
}
model UserPlatformRole {
id Int @id @default(autoincrement())
userId String @map("user_id")
role PlatformRole
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") // User.id de quien asignó (audit trail)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, role])
@@index([role])
@@map("user_platform_roles")
}
/// Registro de acciones críticas para auditoría (SAT compliance, forense, disputas).
/// Se instrumenta vía `utils/audit.ts` con helper fire-and-forget — un fallo al
/// escribir aquí NUNCA debe romper la acción principal.
model AuditLog {
id String @id @default(uuid())
userId String? @map("user_id")
tenantId String? @map("tenant_id")
action String @db.VarChar(64) // "price.updated", "subscription.cancelled", etc.
entityType String? @map("entity_type") @db.VarChar(32)
entityId String? @map("entity_id")
metadata Json? // before/after, ip, userAgent, contexto
createdAt DateTime @default(now()) @map("created_at")
@@index([userId, createdAt])
@@index([tenantId, createdAt])
@@index([action, createdAt])
@@index([entityType, entityId])
@@map("audit_log")
}
/// Padrón persistente de RFCs que ya consumieron su prueba gratuita de 30 días.
/// Sobrevive al ciclo de vida del Tenant (si se borra/recrea, el RFC sigue aquí),
/// bloqueando el abuso de "registro nuevo con el mismo RFC para otro trial".
model TrialUsage {
id Int @id @default(autoincrement())
rfc String @unique @db.VarChar(13)
tenantId String? @map("tenant_id") // Tenant que consumió (null si el tenant se borró después)
startedAt DateTime @default(now()) @map("started_at")
@@map("trial_usages")
}
/// Catálogo despacho — precios + limits editables por admin global.
/// Las `features` siguen viviendo en TS (`DESPACHO_PLANS` en `@horux/shared`)
/// porque están acopladas a UI/middleware y son contrato de código.
/// Incluye filas para `trial` y `custom` (sin precios — null).
model DespachoPlanPrice {
plan String @id // trial | custom | mi_empresa | mi_empresa_plus | business_control | business_cloud
nombre String @db.VarChar(50)
monthly Decimal? @db.Decimal(10, 2)
firstYear Decimal? @db.Decimal(10, 2) @map("first_year")
renewal Decimal? @db.Decimal(10, 2)
permiteMonthly Boolean @default(false) @map("permite_monthly")
/// Limits del plan. -1 = ilimitado donde aplique (maxUsers).
maxRfcs Int @map("max_rfcs")
maxUsers Int @map("max_users")
timbresIncluidosMes Int @default(0) @map("timbres_incluidos_mes")
dbMode DbMode @map("db_mode")
permiteServidorBackup Boolean @default(false) @map("permite_servidor_backup")
/// Habilita SAT incremental (3 syncs/día adicionales al daily). Mi Empresa +,
/// Business Control y Enterprise lo tienen activo por default; planes
/// inferiores se quedan solo con el daily de las 03:00.
permiteSatIncremental Boolean @default(false) @map("permite_sat_incremental")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("despacho_plan_prices")
}
model PlanAddonCatalogo {
id String @id @default(uuid())
codename String @unique @db.VarChar(50)
nombre String
verticalProfile VerticalProfile?
precio Decimal @db.Decimal(10, 2)
frecuencia String @db.VarChar(10)
delta Json
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
subscriptionAddons SubscriptionAddon[]
@@map("plan_addon_catalogo")
}
model ConnectorHeartbeat {
id String @id @default(uuid())
tenantId String @map("tenant_id")
timestamp DateTime @default(now())
latencyMs Int @map("latency_ms")
version String @db.VarChar(20)
pgVersion String? @map("pg_version") @db.VarChar(50)
status String @db.VarChar(20)
errorMsg String? @map("error_msg")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId, timestamp])
@@map("connector_heartbeats")
}
enum PaymentKind {
subscription
timbres_pack
}
model Payment {
id String @id @default(uuid())
tenantId String @map("tenant_id")
subscriptionId String? @map("subscription_id")
mpPaymentId String? @map("mp_payment_id")
amount Decimal @db.Decimal(10, 2)
status String @default("pending")
paymentMethod String? @map("payment_method")
paidAt DateTime? @map("paid_at")
// Tipo de pago. subscription = cobro mensual/anual del plan.
// timbres_pack = compra de paquete de timbres adicionales.
kind PaymentKind @default(subscription)
// ID de la factura emitida auto por Facturapi. Null si no se facturó:
// primer pago (manual), trial sin monto, o fallo al emitir.
facturapiInvoiceId String? @map("facturapi_invoice_id")
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
timbrePaquete TimbrePaquete?
@@index([tenantId])
@@index([subscriptionId])
@@map("payments")
}
/// Catálogo de paquetes de timbres adicionales vendibles. Precios editables
/// desde panel admin. Los 3 defaults (100/$200, 1000/$1400, 10000/$8600) se
/// insertan en seed idempotente.
model TimbrePaqueteCatalogo {
id Int @id @default(autoincrement())
cantidad Int @unique // 100, 1000, 10000
precio Decimal @db.Decimal(10, 2)
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("timbre_paquetes_catalogo")
}
/// Compra individual de timbres adicionales. Los timbres del plan (mensuales)
/// se rastrean en TimbreSuscripcion — esto es SOLO para los extras pagados.
/// Vigencia 1 año desde `adquiridoEn`. El orden de consumo es FIFO por
/// `expiraEn` (menor primero) para no desperdiciar paquetes próximos a vencer.
model TimbrePaquete {
id Int @id @default(autoincrement())
tenantId String @map("tenant_id")
paymentId String? @unique @map("payment_id") // Payment que lo compró; null si admin grant manual
cantidad Int // cuántos timbres tenía originalmente
usados Int @default(0)
precio Decimal @db.Decimal(10, 2) // precio pagado (historial, no cambia si el catálogo cambia)
adquiridoEn DateTime @default(now()) @map("adquirido_en")
expiraEn DateTime @map("expira_en") // adquiridoEn + 1 año
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
payment Payment? @relation(fields: [paymentId], references: [id])
@@index([tenantId, expiraEn])
@@map("timbre_paquetes")
}
model SatSyncJob {
id String @id @default(uuid())
tenantId String @map("tenant_id")
contribuyenteId String? @map("contribuyente_id")
type SatSyncType
status SatSyncStatus @default(pending)
dateFrom DateTime @map("date_from") @db.Date
dateTo DateTime @map("date_to") @db.Date
cfdiType CfdiSyncType? @map("cfdi_type")
satRequestId String? @map("sat_request_id") @db.VarChar(50)
// Mapa { kindKey: requestId } de TODOS los requests creados durante el job.
// Permite que retries reusen requestIds previos en lugar de quemar cuota
// del SAT creando nuevos. kindKey = `${requestType}-${tipoCfdi}-${from}-${to}`.
satRequestIds Json @default("{}") @map("sat_request_ids")
satPackageIds String[] @map("sat_package_ids")
cfdisFound Int @default(0) @map("cfdis_found")
cfdisDownloaded Int @default(0) @map("cfdis_downloaded")
cfdisInserted Int @default(0) @map("cfdis_inserted")
cfdisUpdated Int @default(0) @map("cfdis_updated")
progressPercent Int @default(0) @map("progress_percent")
errorMessage String? @map("error_message")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
retryCount Int @default(0) @map("retry_count")
nextRetryAt DateTime? @map("next_retry_at")
// True cuando el job es `initial` con rango de fechas personalizado por el
// usuario (botón UI). Cambia la política de retry: 2 intentos vs 3 del
// bootstrap puro. Daily/incremental ignoran este campo.
isCustomRange Boolean @default(false) @map("is_custom_range")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([status])
@@index([status, nextRetryAt])
@@map("sat_sync_jobs")
}
enum SatSyncType {
initial
daily
incremental
}
enum SatSyncStatus {
pending
running
completed
failed
}
enum CfdiSyncType {
emitidos
recibidos
}
// ============================================
// Catálogos SAT para Facturación (CFDI 4.0)
// ============================================
model CatFormaPago {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(2)
descripcion String
@@map("cat_forma_pago")
}
model CatMetodoPago {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(3)
descripcion String
@@map("cat_metodo_pago")
}
model CatUsoCfdi {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(4)
descripcion String
personaFisica Boolean @default(true) @map("persona_fisica")
personaMoral Boolean @default(true) @map("persona_moral")
@@map("cat_uso_cfdi")
}
model CatMoneda {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(3)
descripcion String
decimales Int @default(2)
@@map("cat_moneda")
}
model CatClaveUnidad {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(10)
descripcion String
@@map("cat_clave_unidad")
}
model CatClaveProdServ {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(8)
descripcion String
@@index([descripcion])
@@map("cat_clave_prod_serv")
}
model CatObjetoImp {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(2)
descripcion String
@@map("cat_objeto_imp")
}
model CatTipoRelacion {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(2)
descripcion String
@@map("cat_tipo_relacion")
}
model CatExportacion {
id Int @id @default(autoincrement())
clave String @unique @db.VarChar(2)
descripcion String
@@map("cat_exportacion")
}
// ============================================
// Gestión de Timbres Facturapi
// ============================================
model TimbreSuscripcion {
id Int @id @default(autoincrement())
tenantId String @unique @map("tenant_id")
tipo String @db.VarChar(10) // mensual, anual
timbresLimite Int @map("timbres_limite") // 50 o 600
timbresUsados Int @default(0) @map("timbres_usados")
periodoInicio DateTime @map("periodo_inicio") @db.Date
periodoFin DateTime @map("periodo_fin") @db.Date
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@map("timbre_suscripciones")
}

528
apps/api/prisma/seed.ts Normal file
View File

@@ -0,0 +1,528 @@
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: 'BYO' 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: 'BYO' 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();
});