chore: catálogo obligaciones, cierre automático, fixes SAT y facturación
- Catálogo de obligaciones fiscales expandido a 30 entradas con campo requierePago. - Soporte de frecuencia cuatrimestral en obligaciones y declaraciones. - Automatización de cierre de obligaciones fiscales desde Documentos › Declaraciones. - Nuevas tablas obligacion_evidencias, obligacion_periodos estados y declaracion_obligaciones. - Nuevo servicio obligacion-evidencias.service.ts y endpoints REST. - Refactor de declaraciones.service.ts para vincular obligaciones y crear evidencias. - Notificaciones por email para evidencias de obligaciones. - Adjuntar PDFs en correo de declaración subida. - Fix drill-down de CFDIs: carga completa al visualizar. - Fix sincronización SAT: tipos P/N, UUID case-insensitive, no reutilizar requestId. - Fix suscripciones pending en /configuracion/planes-despacho. - Fix sugerencias de Clave Producto SAT: importar catálogo y robustecer autocomplete. - Quitar toggle manual de completado en Configuración › Obligaciones fiscales › Tareas. - Scripts de soporte para Demo Ventas y utilerías (change-user-email, resend-welcome, import-clave-prod-serv). - Documentación de cambios en docs/CAMBIOS-2026-05-04.md.
This commit is contained in:
75
apps/api/scripts/change-user-email.ts
Normal file
75
apps/api/scripts/change-user-email.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Script: change-user-email
|
||||
*
|
||||
* Cambia el correo de un usuario, resetea su contraseña a una temporal
|
||||
* y reenvía el correo de bienvenida con las nuevas credenciales.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/change-user-email.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { emailService } from '../src/services/email/email.service.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const OLD_EMAIL = 'eduardo.corona@corpcyl.com';
|
||||
const NEW_EMAIL = 'miguel.corona@corpcyl.com';
|
||||
|
||||
function generateTempPassword(length = 12): string {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
||||
let result = '';
|
||||
const bytes = randomBytes(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[bytes[i] % chars.length];
|
||||
}
|
||||
return result + '!';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const user = await prisma.user.findUnique({ where: { email: OLD_EMAIL } });
|
||||
if (!user) {
|
||||
console.error(`❌ No existe un usuario con el correo ${OLD_EMAIL}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { email: NEW_EMAIL } });
|
||||
if (existing) {
|
||||
console.error(`❌ Ya existe un usuario con el correo ${NEW_EMAIL}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tempPassword = generateTempPassword();
|
||||
const passwordHash = await bcrypt.hash(tempPassword, 12);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
email: NEW_EMAIL,
|
||||
passwordHash,
|
||||
tokenVersion: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await emailService.sendWelcome(NEW_EMAIL, {
|
||||
nombre: user.nombre,
|
||||
email: NEW_EMAIL,
|
||||
tempPassword,
|
||||
});
|
||||
|
||||
console.log('✅ Correo actualizado:', OLD_EMAIL, '→', NEW_EMAIL);
|
||||
console.log('✅ Contraseña temporal generada y enviada por correo');
|
||||
console.log(' Nombre:', user.nombre);
|
||||
console.log(' Email:', NEW_EMAIL);
|
||||
console.log(' Contraseña temporal:', tempPassword);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
457
apps/api/scripts/create-demo-ventas.ts
Normal file
457
apps/api/scripts/create-demo-ventas.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Script: create-demo-ventas
|
||||
*
|
||||
* Crea una cuenta demo completa para ventas:
|
||||
* - Tenant "Demo Ventas SA de CV" (plan custom, sin cobro)
|
||||
* - Usuario owner: demo@horuxfin.com / Demo12345!
|
||||
* - Base de datos propia con datos ficticios de contabilidad
|
||||
* - Contribuyente, clientes/proveedores, CFDIs, bancos, conciliaciones,
|
||||
* obligaciones fiscales y cartera.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/create-demo-ventas.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Pool } from 'pg';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { tenantDb } from '../src/config/database.ts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const DEMO = {
|
||||
rfc: 'DEMO2501019X2',
|
||||
nombre: 'Demo Ventas SA de CV',
|
||||
email: 'demo@horuxfin.com',
|
||||
password: 'Demo12345!',
|
||||
databaseName: 'horux_demoventas',
|
||||
codigoPostal: '01000',
|
||||
};
|
||||
|
||||
const CLIENTES = [
|
||||
{ rfc: 'CLI123456AB1', nombre: 'Cliente Alfa SA' },
|
||||
{ rfc: 'CLI123456AB2', nombre: 'Cliente Beta SA' },
|
||||
{ rfc: 'CLI123456AB3', nombre: 'Cliente Gamma SA' },
|
||||
{ rfc: 'CLI123456AB4', nombre: 'Cliente Delta SA' },
|
||||
{ rfc: 'CLI123456AB5', nombre: 'Cliente Epsilon SA' },
|
||||
];
|
||||
|
||||
const PROVEEDORES = [
|
||||
{ rfc: 'PRO123456AB1', nombre: 'Proveedor Materiales SA' },
|
||||
{ rfc: 'PRO123456AB2', nombre: 'Proveedor Servicios SA' },
|
||||
{ rfc: 'PRO123456AB3', nombre: 'Proveedor Logistica SA' },
|
||||
{ rfc: 'PRO123456AB4', nombre: 'Proveedor Tecnologia SA' },
|
||||
{ rfc: 'PRO123456AB5', nombre: 'Proveedor Papeleria SA' },
|
||||
];
|
||||
|
||||
const PRODUCTOS = [
|
||||
{ clave: '84111506', descripcion: 'Servicio de consultoria', unidad: 'Servicio' },
|
||||
{ clave: '43232408', descripcion: 'Licencia de software', unidad: 'Licencia' },
|
||||
{ clave: '81141500', descripcion: 'Soporte tecnico', unidad: 'Servicio' },
|
||||
{ clave: '81121700', descripcion: 'Desarrollo web', unidad: 'Servicio' },
|
||||
{ clave: '86101500', descripcion: 'Capacitacion', unidad: 'Servicio' },
|
||||
{ clave: '50151500', descripcion: 'Materiales de oficina', unidad: 'Pieza' },
|
||||
{ clave: '80181600', descripcion: 'Publicidad', unidad: 'Servicio' },
|
||||
{ clave: '81112200', descripcion: 'Diseno grafico', unidad: 'Servicio' },
|
||||
];
|
||||
|
||||
function parseDatabaseUrl(url: string) {
|
||||
const parsed = new URL(url);
|
||||
return {
|
||||
host: parsed.hostname,
|
||||
port: parseInt(parsed.port || '5432'),
|
||||
user: decodeURIComponent(parsed.username),
|
||||
password: decodeURIComponent(parsed.password),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Creando cuenta demo "Demo Ventas"...\n');
|
||||
|
||||
const ownerRole = await prisma.rol.findUnique({ where: { nombre: 'owner' } });
|
||||
if (!ownerRole) throw new Error('Rol owner no encontrado en BD central');
|
||||
|
||||
// ============================================================
|
||||
// 1. Tenant
|
||||
// ============================================================
|
||||
let tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO.rfc } });
|
||||
if (!tenant) {
|
||||
tenant = await prisma.tenant.create({
|
||||
data: {
|
||||
nombre: DEMO.nombre,
|
||||
rfc: DEMO.rfc,
|
||||
plan: 'custom',
|
||||
databaseName: DEMO.databaseName,
|
||||
verticalProfile: 'CONTABLE',
|
||||
dbMode: 'MANAGED',
|
||||
dbSchemaVersion: 0,
|
||||
codigoPostal: DEMO.codigoPostal,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('✅ Tenant creado:', tenant.nombre, `(${tenant.rfc})`);
|
||||
} else {
|
||||
await prisma.tenant.update({
|
||||
where: { id: tenant.id },
|
||||
data: { plan: 'custom', active: true, verticalProfile: 'CONTABLE' },
|
||||
});
|
||||
console.log('✅ Tenant actualizado:', tenant.nombre, `(${tenant.rfc})`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. Usuario owner
|
||||
// ============================================================
|
||||
let user = await prisma.user.findUnique({ where: { email: DEMO.email } });
|
||||
const passwordHash = await bcrypt.hash(DEMO.password, 12);
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
email: DEMO.email,
|
||||
passwordHash,
|
||||
nombre: 'Usuario Demo',
|
||||
lastTenantId: tenant.id,
|
||||
},
|
||||
});
|
||||
console.log('✅ Usuario creado:', user.email);
|
||||
} else {
|
||||
user = await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { passwordHash, lastTenantId: tenant.id },
|
||||
});
|
||||
console.log('✅ Usuario actualizado:', user.email);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. Membership
|
||||
// ============================================================
|
||||
await prisma.tenantMembership.upsert({
|
||||
where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } },
|
||||
update: { rolId: ownerRole.id, isOwner: true, active: true },
|
||||
create: {
|
||||
userId: user.id,
|
||||
tenantId: tenant.id,
|
||||
rolId: ownerRole.id,
|
||||
isOwner: true,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
console.log('✅ Membership owner asignada');
|
||||
|
||||
// ============================================================
|
||||
// 4. Suscripción custom gratis/ilimitada (status authorized)
|
||||
// ============================================================
|
||||
const now = new Date();
|
||||
const periodEnd = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
const existingSub = await prisma.subscription.findFirst({
|
||||
where: { tenantId: tenant.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!existingSub) {
|
||||
await prisma.subscription.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
plan: 'custom',
|
||||
status: 'authorized',
|
||||
amount: 0,
|
||||
frequency: 'monthly',
|
||||
currentPeriodStart: now,
|
||||
currentPeriodEnd: periodEnd,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.subscription.update({
|
||||
where: { id: existingSub.id },
|
||||
data: {
|
||||
plan: 'custom',
|
||||
status: 'authorized',
|
||||
amount: 0,
|
||||
currentPeriodStart: now,
|
||||
currentPeriodEnd: periodEnd,
|
||||
pendingPlan: null,
|
||||
pendingFrequency: null,
|
||||
pendingEffectiveAt: null,
|
||||
upgradePreferenceId: null,
|
||||
upgradeTargetPlan: null,
|
||||
upgradeTargetAmount: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log('✅ Suscripción custom activa (gratis)');
|
||||
|
||||
// ============================================================
|
||||
// 5. Régimen fiscal activo del tenant
|
||||
// ============================================================
|
||||
const regimen = await prisma.regimen.findUnique({ where: { clave: '601' } });
|
||||
if (regimen) {
|
||||
await prisma.tenantRegimenActivo.upsert({
|
||||
where: { tenantId_regimenId: { tenantId: tenant.id, regimenId: regimen.id } },
|
||||
update: {},
|
||||
create: { tenantId: tenant.id, regimenId: regimen.id },
|
||||
});
|
||||
console.log('✅ Régimen 601 activado para el tenant');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 6. Base de datos del tenant
|
||||
// ============================================================
|
||||
await tenantDb.provisionDatabase(DEMO.rfc, DEMO.databaseName);
|
||||
const pool = await tenantDb.getPool(tenant.id, DEMO.databaseName);
|
||||
console.log('✅ Base de datos del tenant provisionada:', DEMO.databaseName);
|
||||
|
||||
// ============================================================
|
||||
// 7. Datos ficticios en BD del tenant
|
||||
// ============================================================
|
||||
await seedTenantData(pool, tenant.id, user.id);
|
||||
|
||||
console.log('\n🎉 Demo Ventas lista');
|
||||
console.log(' Login:', DEMO.email, '/', DEMO.password);
|
||||
console.log(' Tenant:', DEMO.nombre, `(${DEMO.rfc})`);
|
||||
console.log(' BD:', DEMO.databaseName);
|
||||
}
|
||||
|
||||
async function seedTenantData(pool: Pool, tenantId: string, ownerId: string) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Contribuyente principal
|
||||
const { rows: [entidad] } = await client.query<{ id: string }>(`
|
||||
INSERT INTO entidades_gestionadas (tipo, nombre, identificador, supervisor_user_id)
|
||||
VALUES ('CONTRIBUYENTE', $1, $2, $3)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
`, [DEMO.nombre, DEMO.rfc, ownerId]);
|
||||
|
||||
let contribuyenteId: string;
|
||||
if (entidad) {
|
||||
contribuyenteId = entidad.id;
|
||||
await client.query(`
|
||||
INSERT INTO contribuyentes (entidad_id, rfc, regimen_fiscal, codigo_postal)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (entidad_id) DO NOTHING
|
||||
`, [contribuyenteId, DEMO.rfc, '601', DEMO.codigoPostal]);
|
||||
} else {
|
||||
const { rows: [existing] } = await client.query<{ id: string }>(`
|
||||
SELECT e.id FROM entidades_gestionadas e
|
||||
JOIN contribuyentes c ON c.entidad_id = e.id
|
||||
WHERE e.identificador = $1
|
||||
`, [DEMO.rfc]);
|
||||
contribuyenteId = existing.id;
|
||||
}
|
||||
console.log('✅ Contribuyente principal creado:', DEMO.rfc);
|
||||
|
||||
// RFCs de clientes y proveedores
|
||||
const rfcs = new Map<string, number>();
|
||||
for (const c of [...CLIENTES, ...PROVEEDORES]) {
|
||||
const { rows: [r] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO rfcs (rfc, razon_social, regimen_fiscal)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social
|
||||
RETURNING id
|
||||
`, [c.rfc, c.nombre, c.rfc.startsWith('CLI') ? '601' : '601']);
|
||||
rfcs.set(c.rfc, r.id);
|
||||
}
|
||||
// RFC del contribuyente principal
|
||||
const { rows: [rfcPrincipal] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO rfcs (rfc, razon_social, regimen_fiscal)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social
|
||||
RETURNING id
|
||||
`, [DEMO.rfc, DEMO.nombre, '601']);
|
||||
rfcs.set(DEMO.rfc, rfcPrincipal.id);
|
||||
|
||||
// Bancos del contribuyente
|
||||
const { rows: [banco1] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO bancos (banco, terminacion_cuenta, contribuyente_id)
|
||||
VALUES ($1, $2, $3) RETURNING id
|
||||
`, ['BBVA', '1234', contribuyenteId]);
|
||||
const { rows: [banco2] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO bancos (banco, terminacion_cuenta, contribuyente_id)
|
||||
VALUES ($1, $2, $3) RETURNING id
|
||||
`, ['Santander', '5678', contribuyenteId]);
|
||||
console.log('✅ Bancos creados');
|
||||
|
||||
// Generar CFDIs
|
||||
const tipos: Array<'EMITIDO' | 'RECIBIDO'> = ['EMITIDO', 'RECIBIDO'];
|
||||
const cfdiIds: number[] = [];
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const tipo = tipos[i % 2];
|
||||
const esEmitido = tipo === 'EMITIDO';
|
||||
const contraparte = esEmitido
|
||||
? CLIENTES[i % CLIENTES.length]
|
||||
: PROVEEDORES[i % PROVEEDORES.length];
|
||||
|
||||
const subtotal = Math.floor(Math.random() * 40000) + 2000;
|
||||
const iva = Math.round(subtotal * 0.16 * 100) / 100;
|
||||
const total = Math.round((subtotal + iva) * 100) / 100;
|
||||
|
||||
const daysAgo = Math.floor(Math.random() * 540); // hasta ~18 meses atrás
|
||||
const fecha = new Date();
|
||||
fecha.setDate(fecha.getDate() - daysAgo);
|
||||
fecha.setHours(10 + (i % 8), 0, 0, 0);
|
||||
|
||||
const year = String(fecha.getFullYear());
|
||||
const month = String(fecha.getMonth() + 1).padStart(2, '0');
|
||||
const fechaStr = fecha.toISOString();
|
||||
|
||||
const metodoPago = Math.random() > 0.3 ? 'PUE' : 'PPD';
|
||||
const formasPago = ['01', '02', '03', '04'];
|
||||
const formaPago = formasPago[i % formasPago.length];
|
||||
const usoCfdi = esEmitido ? 'G03' : 'G01';
|
||||
|
||||
const rfcEmisor = esEmitido ? DEMO.rfc : contraparte.rfc;
|
||||
const nombreEmisor = esEmitido ? DEMO.nombre : contraparte.nombre;
|
||||
const rfcReceptor = esEmitido ? contraparte.rfc : DEMO.rfc;
|
||||
const nombreReceptor = esEmitido ? contraparte.nombre : DEMO.nombre;
|
||||
|
||||
const { rows: [cfdi] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO cfdis (
|
||||
year, month, type, uuid, serie, folio, status, fecha_emision,
|
||||
rfc_emisor_id, rfc_emisor, nombre_emisor,
|
||||
rfc_receptor_id, rfc_receptor, nombre_receptor,
|
||||
subtotal, subtotal_mxn, descuento, descuento_mxn,
|
||||
total, total_mxn, moneda, tipo_cambio, tipo_comprobante,
|
||||
metodo_pago, forma_pago, uso_cfdi,
|
||||
iva_traslado, iva_traslado_mxn,
|
||||
regimen_fiscal_emisor, regimen_fiscal_receptor,
|
||||
contribuyente_id, fecha_efectiva, meses_global, año_global
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
$9, $10, $11,
|
||||
$12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20, $21, $22, $23,
|
||||
$24, $25, $26,
|
||||
$27, $28,
|
||||
$29, $30,
|
||||
$31, $32, $33, $34
|
||||
) RETURNING id
|
||||
`, [
|
||||
year, month, tipo, randomUUID(), 'DEMO', String(1000 + i),
|
||||
'Vigente', fechaStr,
|
||||
rfcs.get(rfcEmisor), rfcEmisor, nombreEmisor,
|
||||
rfcs.get(rfcReceptor), rfcReceptor, nombreReceptor,
|
||||
subtotal, subtotal, 0, 0,
|
||||
total, total, 'MXN', 1, 'I',
|
||||
metodoPago, formaPago, usoCfdi,
|
||||
iva, iva,
|
||||
'601', '601',
|
||||
contribuyenteId, fechaStr, month, year,
|
||||
]);
|
||||
cfdiIds.push(cfdi.id);
|
||||
|
||||
// Conceptos
|
||||
const numConceptos = Math.floor(Math.random() * 3) + 1;
|
||||
for (let j = 0; j < numConceptos; j++) {
|
||||
const prod = PRODUCTOS[(i + j) % PRODUCTOS.length];
|
||||
const cantidad = Math.floor(Math.random() * 5) + 1;
|
||||
const valorUnitario = Math.floor(Math.random() * 4000) + 500;
|
||||
const importe = Math.round(cantidad * valorUnitario * 100) / 100;
|
||||
const ivaConcepto = Math.round(importe * 0.16 * 100) / 100;
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO cfdi_conceptos (
|
||||
cfdi_id, clave_prod_serv, descripcion, cantidad, clave_unidad, unidad,
|
||||
valor_unitario, valor_unitario_mxn, importe, importe_mxn,
|
||||
iva_traslado, iva_traslado_mxn
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
`, [
|
||||
cfdi.id, prod.clave, prod.descripcion, cantidad, 'E48', prod.unidad,
|
||||
valorUnitario, valorUnitario, importe, importe,
|
||||
ivaConcepto, ivaConcepto,
|
||||
]);
|
||||
}
|
||||
}
|
||||
console.log('✅ 60 CFDIs y conceptos creados');
|
||||
|
||||
// Conciliaciones para algunos CFDIs PPD pagados con transferencia (forma 02/03)
|
||||
const { rows: cfdisPpd } = await client.query<{ id: number; year: string; month: string }>(`
|
||||
SELECT id, year, month FROM cfdis
|
||||
WHERE metodo_pago = 'PPD' AND forma_pago IN ('02', '03')
|
||||
ORDER BY id LIMIT 15
|
||||
`);
|
||||
|
||||
for (const c of cfdisPpd) {
|
||||
const bancoId = Math.random() > 0.5 ? banco1.id : banco2.id;
|
||||
const fechaPago = new Date();
|
||||
fechaPago.setDate(fechaPago.getDate() - Math.floor(Math.random() * 30));
|
||||
await client.query(`
|
||||
INSERT INTO conciliaciones (anio, mes, id_cfdi, fecha_de_pago, id_banco)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (id_cfdi) DO NOTHING
|
||||
`, [c.year, c.month, c.id, fechaPago.toISOString().split('T')[0], bancoId]);
|
||||
}
|
||||
console.log('✅ Conciliaciones creadas');
|
||||
|
||||
// Obligaciones fiscales asignadas al contribuyente
|
||||
const obligaciones = [
|
||||
{ id: 'isr-provisional', nombre: 'Pago provisional de ISR', categoria: 'Federal mensual' },
|
||||
{ id: 'iva-mensual', nombre: 'Pago mensual definitivo de IVA', categoria: 'Federal mensual' },
|
||||
{ id: 'ret-isr-honorarios', nombre: 'Retenciones de ISR por honorarios y arrendamiento a PF', categoria: 'Federal mensual' },
|
||||
{ id: 'diot', nombre: 'DIOT', categoria: 'Informativa mensual' },
|
||||
{ id: 'imss-cuotas', nombre: 'Cuotas obrero-patronales IMSS', categoria: 'Seguridad social' },
|
||||
{ id: 'anual-isr-pm', nombre: 'Declaración Anual de ISR PM', categoria: 'Anual' },
|
||||
{ id: 'isn', nombre: 'ISN - Impuesto Sobre Nómina', categoria: 'Estatal' },
|
||||
{ id: 'isrtp', nombre: 'Impuesto sobre remuneración al trabajo', categoria: 'Estatal' },
|
||||
];
|
||||
|
||||
for (const o of obligaciones) {
|
||||
await client.query(`
|
||||
INSERT INTO obligaciones_contribuyente (
|
||||
contribuyente_id, catalogo_id, nombre, frecuencia, fecha_limite, categoria, activa, es_recomendada
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, true, true)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [contribuyenteId, o.id, o.nombre, 'mensual', 'Día 17 del mes siguiente', o.categoria]);
|
||||
}
|
||||
console.log('✅ Obligaciones fiscales asignadas');
|
||||
|
||||
// Cartera principal con el contribuyente
|
||||
const { rows: [cartera] } = await client.query<{ id: string }>(`
|
||||
INSERT INTO carteras (supervisor_user_id, nombre, descripcion)
|
||||
VALUES ($1, $2, $3) RETURNING id
|
||||
`, [ownerId, 'Cartera Principal', 'Clientes y prospectos de Demo Ventas']);
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO cartera_entidades (cartera_id, entidad_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [cartera.id, contribuyenteId]);
|
||||
console.log('✅ Cartera principal creada');
|
||||
|
||||
// Alertas y recordatorios de ejemplo
|
||||
await client.query(`
|
||||
INSERT INTO alertas (tipo, titulo, mensaje, prioridad, fecha_vencimiento)
|
||||
VALUES
|
||||
('obligacion', 'Declaración mensual de IVA', 'Pago de IVA correspondiente a mayo 2026', 'alta', NOW() + INTERVAL '10 days'),
|
||||
('obligacion', 'Pago provisional ISR', 'Pago provisional de ISR de mayo 2026', 'alta', NOW() + INTERVAL '10 days'),
|
||||
('sat', 'Sincronización SAT pendiente', 'Última sincronización hace más de 7 días', 'media', NOW() + INTERVAL '3 days')
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO recordatorios (titulo, descripcion, fecha_limite, notas, completado, privado, creado_por)
|
||||
VALUES
|
||||
('Revisar estados de cuenta', 'Conciliar pagos de clientes', NOW() + INTERVAL '5 days', 'Prioridad alta', false, false, $1),
|
||||
('Enviar facturas del mes', 'Facturación recurrente a clientes', NOW() + INTERVAL '7 days', 'Clientes Alfa y Beta', false, false, $1)
|
||||
`, [ownerId]);
|
||||
console.log('✅ Alertas y recordatorios de ejemplo creados');
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error creando demo:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await tenantDb.shutdown();
|
||||
});
|
||||
126
apps/api/scripts/fix-demo-carteras-asignaciones.ts
Normal file
126
apps/api/scripts/fix-demo-carteras-asignaciones.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Script: fix-demo-carteras-asignaciones
|
||||
*
|
||||
* Corrige la estructura de carteras de Demo Ventas para que las asignaciones
|
||||
* de obligaciones/tareas al auxiliar sean válidas:
|
||||
* - La cartera principal queda solo para el supervisor.
|
||||
* - Se crea una subcartera asignada al auxiliar.
|
||||
* - Los contribuyentes se mueven a la subcartera del auxiliar.
|
||||
* - Se mantiene la relación auxiliar → supervisor.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/fix-demo-carteras-asignaciones.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { tenantDb } from '../src/config/database.ts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const DEMO_RFC = 'DEMO2501019X2';
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 Corrigiendo carteras y asignaciones de Demo Ventas...\n');
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO_RFC } });
|
||||
if (!tenant) throw new Error(`Tenant ${DEMO_RFC} no encontrado`);
|
||||
|
||||
const [supervisor, auxiliar] = await Promise.all([
|
||||
prisma.user.findUnique({ where: { email: 'supervisor@horuxfin.com' } }),
|
||||
prisma.user.findUnique({ where: { email: 'auxiliar@horuxfin.com' } }),
|
||||
]);
|
||||
if (!supervisor) throw new Error('Usuario supervisor no encontrado');
|
||||
if (!auxiliar) throw new Error('Usuario auxiliar no encontrado');
|
||||
|
||||
const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Obtener cartera principal
|
||||
const { rows: [carteraPrincipal] } = await client.query<{ id: string }>(`
|
||||
SELECT id FROM carteras WHERE parent_id IS NULL ORDER BY created_at LIMIT 1
|
||||
`);
|
||||
if (!carteraPrincipal) throw new Error('No existe cartera principal');
|
||||
|
||||
// Crear subcartera para el auxiliar
|
||||
const { rows: [subcartera] } = await client.query<{ id: string }>(`
|
||||
INSERT INTO carteras (supervisor_user_id, auxiliar_user_id, nombre, descripcion, parent_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
`, [supervisor.id, auxiliar.id, 'Cartera Auxiliar Demo', 'RFCs asignados al auxiliar de demo', carteraPrincipal.id]);
|
||||
|
||||
const subcarteraId = subcartera?.id;
|
||||
if (!subcarteraId) {
|
||||
// Si ya existía, recuperarla
|
||||
const { rows: [existing] } = await client.query<{ id: string }>(`
|
||||
SELECT id FROM carteras WHERE parent_id = $1 AND auxiliar_user_id = $2 LIMIT 1
|
||||
`, [carteraPrincipal.id, auxiliar.id]);
|
||||
if (!existing) throw new Error('No se pudo crear ni recuperar la subcartera del auxiliar');
|
||||
// Asegurar que tenga supervisor
|
||||
await client.query(`UPDATE carteras SET supervisor_user_id = $1 WHERE id = $2`, [supervisor.id, existing.id]);
|
||||
}
|
||||
const finalSubcarteraId = subcarteraId || (await client.query<{ id: string }>(`SELECT id FROM carteras WHERE parent_id = $1 AND auxiliar_user_id = $2 LIMIT 1`, [carteraPrincipal.id, auxiliar.id])).rows[0].id;
|
||||
|
||||
console.log('✅ Subcartera del auxiliar creada/recuperada');
|
||||
|
||||
// Mover contribuyentes de la cartera principal a la subcartera del auxiliar
|
||||
const { rows: entidades } = await client.query<{ entidad_id: string }>(`
|
||||
SELECT entidad_id FROM cartera_entidades WHERE cartera_id = $1
|
||||
`, [carteraPrincipal.id]);
|
||||
|
||||
for (const e of entidades) {
|
||||
await client.query(`
|
||||
INSERT INTO cartera_entidades (cartera_id, entidad_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [finalSubcarteraId, e.entidad_id]);
|
||||
}
|
||||
|
||||
// Quitar contribuyentes de la cartera principal (ahora están en la subcartera)
|
||||
await client.query(`DELETE FROM cartera_entidades WHERE cartera_id = $1`, [carteraPrincipal.id]);
|
||||
|
||||
// La cartera principal ya no tiene auxiliar asignado
|
||||
await client.query(`UPDATE carteras SET auxiliar_user_id = NULL WHERE id = $1`, [carteraPrincipal.id]);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
console.log(`✅ ${entidades.length} contribuyentes movidos a la subcartera del auxiliar`);
|
||||
console.log('✅ Cartera principal limpia (sin auxiliar)');
|
||||
|
||||
// Asegurar relación auxiliar → supervisor
|
||||
await pool.query(`
|
||||
INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = EXCLUDED.supervisor_user_id
|
||||
`, [auxiliar.id, supervisor.id]);
|
||||
console.log('✅ Relación auxiliar → supervisor registrada');
|
||||
|
||||
// Validar: el auxiliar debe ser elegible para todos los contribuyentes
|
||||
const { rows: elegibles } = await pool.query<{ entidad_id: string }>(`
|
||||
SELECT DISTINCT ce.entidad_id
|
||||
FROM carteras c
|
||||
JOIN cartera_entidades ce ON ce.cartera_id = c.id
|
||||
WHERE c.auxiliar_user_id = $1
|
||||
`, [auxiliar.id]);
|
||||
console.log(`✅ Auxiliar elegible para ${elegibles.length} contribuyentes`);
|
||||
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
console.log('\n🎉 Estructura de carteras corregida');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await tenantDb.shutdown();
|
||||
});
|
||||
56
apps/api/scripts/import-clave-prod-serv.ts
Normal file
56
apps/api/scripts/import-clave-prod-serv.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import { prisma } from '../src/config/database.js';
|
||||
|
||||
const BATCH_SIZE = 2000;
|
||||
const CSV_PATH = process.argv[2] || '/tmp/claves_prod_serv.csv';
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(CSV_PATH)) {
|
||||
console.error(`Archivo no encontrado: ${CSV_PATH}`);
|
||||
console.error('Uso: npx tsx scripts/import-clave-prod-serv.ts [ruta/al/csv]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const existing = await prisma.catClaveProdServ.count();
|
||||
console.log(`Registros existentes: ${existing}`);
|
||||
if (existing > 0) {
|
||||
console.log('El catálogo ya tiene datos. No se importará nada.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(CSV_PATH, { encoding: 'utf-8' });
|
||||
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
||||
|
||||
let batch: { clave: string; descripcion: string }[] = [];
|
||||
let total = 0;
|
||||
|
||||
for await (const line of rl) {
|
||||
const idx = line.indexOf(',');
|
||||
if (idx === -1) continue;
|
||||
const clave = line.slice(0, idx).trim();
|
||||
const descripcion = line.slice(idx + 1).trim();
|
||||
if (!clave || !descripcion) continue;
|
||||
batch.push({ clave, descripcion });
|
||||
|
||||
if (batch.length >= BATCH_SIZE) {
|
||||
await prisma.catClaveProdServ.createMany({ data: batch, skipDuplicates: true });
|
||||
total += batch.length;
|
||||
console.log(`Importados: ${total}`);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length > 0) {
|
||||
await prisma.catClaveProdServ.createMany({ data: batch, skipDuplicates: true });
|
||||
total += batch.length;
|
||||
}
|
||||
|
||||
console.log(`Importación completada. Total: ${total}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
67
apps/api/scripts/resend-welcome.ts
Normal file
67
apps/api/scripts/resend-welcome.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Script: resend-welcome
|
||||
*
|
||||
* Genera una nueva contraseña temporal para el usuario y reenvía el correo
|
||||
* de bienvenida. Útil cuando el envío anterior falló o se perdió.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/resend-welcome.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { emailService } from '../src/services/email/email.service.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const EMAIL = 'miguel.corona@corpcyl.com';
|
||||
|
||||
function generateTempPassword(length = 12): string {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
||||
let result = '';
|
||||
const bytes = randomBytes(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[bytes[i] % chars.length];
|
||||
}
|
||||
return result + '!';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const user = await prisma.user.findUnique({ where: { email: EMAIL } });
|
||||
if (!user) {
|
||||
console.error(`❌ No existe un usuario con el correo ${EMAIL}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tempPassword = generateTempPassword();
|
||||
const passwordHash = await bcrypt.hash(tempPassword, 12);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
passwordHash,
|
||||
tokenVersion: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
console.log('⏳ Enviando correo de bienvenida a', EMAIL, '...');
|
||||
await emailService.sendWelcome(EMAIL, {
|
||||
nombre: user.nombre,
|
||||
email: EMAIL,
|
||||
tempPassword,
|
||||
});
|
||||
|
||||
console.log('✅ Correo de bienvenida enviado');
|
||||
console.log(' Nombre:', user.nombre);
|
||||
console.log(' Email:', EMAIL);
|
||||
console.log(' Contraseña temporal:', tempPassword);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error enviando correo:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
112
apps/api/scripts/reset-demo-asignaciones.ts
Normal file
112
apps/api/scripts/reset-demo-asignaciones.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Script: reset-demo-asignaciones
|
||||
*
|
||||
* Deja el tenant Demo Ventas listo para que el usuario haga manualmente
|
||||
* el flujo de asignación de carteras, obligaciones y tareas (útiles para tutoriales):
|
||||
* - Elimina la subcartera del auxiliar.
|
||||
* - Deja todos los contribuyentes en la cartera principal (sin auxiliar).
|
||||
* - Elimina asignaciones de obligaciones y tareas.
|
||||
* - Elimina la relación auxiliar → supervisor.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/reset-demo-asignaciones.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { tenantDb } from '../src/config/database.ts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const DEMO_RFC = 'DEMO2501019X2';
|
||||
|
||||
async function findUserIdByEmail(email: string): Promise<string | null> {
|
||||
const rows = await prisma.$queryRawUnsafe<{ id: string }[]>(
|
||||
`SELECT id FROM users WHERE email = $1 LIMIT 1`,
|
||||
email,
|
||||
);
|
||||
return rows[0]?.id ?? null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔄 Reseteando asignaciones de Demo Ventas para tutoriales...\n');
|
||||
|
||||
const tenants = await prisma.$queryRawUnsafe<{ id: string; database_name: string }[]>(
|
||||
`SELECT id, database_name FROM tenants WHERE rfc = $1 LIMIT 1`,
|
||||
DEMO_RFC,
|
||||
);
|
||||
const tenant = tenants[0];
|
||||
if (!tenant) throw new Error(`Tenant ${DEMO_RFC} no encontrado`);
|
||||
|
||||
const supervisorId = await findUserIdByEmail('supervisor@horuxfin.com');
|
||||
if (!supervisorId) throw new Error('Usuario supervisor no encontrado');
|
||||
|
||||
const auxiliarId = await findUserIdByEmail('auxiliar@horuxfin.com');
|
||||
|
||||
const pool = await tenantDb.getPool(tenant.id, tenant.database_name);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Eliminar asignaciones de obligaciones y tareas
|
||||
await client.query('DELETE FROM obligacion_asignaciones');
|
||||
await client.query('DELETE FROM tarea_asignaciones');
|
||||
console.log('✅ Asignaciones de obligaciones y tareas eliminadas');
|
||||
|
||||
// Obtener cartera principal
|
||||
const { rows: [carteraPrincipal] } = await client.query<{ id: string }>(`
|
||||
SELECT id FROM carteras WHERE parent_id IS NULL ORDER BY created_at LIMIT 1
|
||||
`);
|
||||
if (!carteraPrincipal) throw new Error('No existe cartera principal');
|
||||
|
||||
// Eliminar subcarteras (borra también cartera_entidades en cascade si hay FK)
|
||||
await client.query('DELETE FROM cartera_entidades WHERE cartera_id != $1', [carteraPrincipal.id]);
|
||||
await client.query('DELETE FROM carteras WHERE parent_id = $1', [carteraPrincipal.id]);
|
||||
console.log('✅ Subcarteras eliminadas');
|
||||
|
||||
// Limpiar cartera principal: sin auxiliar, supervisor demo
|
||||
await client.query(`
|
||||
UPDATE carteras SET auxiliar_user_id = NULL, supervisor_user_id = $1 WHERE id = $2
|
||||
`, [supervisorId, carteraPrincipal.id]);
|
||||
|
||||
// Agregar todos los contribuyentes a la cartera principal
|
||||
const { rows: contribuyentes } = await client.query<{ entidad_id: string }>(`
|
||||
SELECT entidad_id FROM contribuyentes
|
||||
`);
|
||||
|
||||
for (const c of contribuyentes) {
|
||||
await client.query(`
|
||||
INSERT INTO cartera_entidades (cartera_id, entidad_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [carteraPrincipal.id, c.entidad_id]);
|
||||
}
|
||||
console.log(`✅ ${contribuyentes.length} contribuyentes dejados en Cartera Principal`);
|
||||
|
||||
// Eliminar relación auxiliar → supervisor para que se cree en el tutorial
|
||||
if (auxiliarId) {
|
||||
await client.query('DELETE FROM auxiliar_supervisores WHERE auxiliar_user_id = $1', [auxiliarId]);
|
||||
console.log('✅ Relación auxiliar → supervisor eliminada');
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
console.log('\n🎉 Demo Ventas listo para tutoriales');
|
||||
console.log(' - Cartera Principal con 6 contribuyentes, sin auxiliar');
|
||||
console.log(' - 48 obligaciones y 24 tareas sin asignar');
|
||||
console.log(' - Usuarios: owner, supervisor, auxiliar, cliente');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await tenantDb.shutdown();
|
||||
});
|
||||
124
apps/api/scripts/seed-demo-obligaciones-tareas.ts
Normal file
124
apps/api/scripts/seed-demo-obligaciones-tareas.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Script: seed-demo-obligaciones-tareas
|
||||
*
|
||||
* Crea obligaciones fiscales y tareas recurrentes para todos los contribuyentes
|
||||
* del tenant Demo Ventas. Además asigna el usuario auxiliar a las tareas y
|
||||
* obligaciones, y lo vincula a la cartera principal.
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/seed-demo-obligaciones-tareas.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { tenantDb } from '../src/config/database.ts';
|
||||
import { seedTareasDefault, materializarPeriodos } from '../src/services/tareas.service.ts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const DEMO_RFC = 'DEMO2501019X2';
|
||||
|
||||
const OBLIGACIONES = [
|
||||
{ id: 'isr-provisional', nombre: 'Pago provisional de ISR', fundamento: 'Art. 14 LISR', frecuencia: 'mensual', fechaLimite: 'Día 17 del mes siguiente', categoria: 'Federal mensual' },
|
||||
{ id: 'iva-mensual', nombre: 'Pago mensual definitivo de IVA', fundamento: 'Art. 5-D LIVA', frecuencia: 'mensual', fechaLimite: 'Día 17 del mes siguiente', categoria: 'Federal mensual' },
|
||||
{ id: 'ret-isr-honorarios', nombre: 'Retenciones de ISR por honorarios y arrendamiento a PF', fundamento: 'Art. 106/116 LISR', frecuencia: 'mensual', fechaLimite: 'Día 17 del mes siguiente', categoria: 'Federal mensual' },
|
||||
{ id: 'diot', nombre: 'DIOT (Declaración Informativa de Operaciones con Terceros)', fundamento: 'Art. 32 LIVA', frecuencia: 'mensual', fechaLimite: 'Último día del mes siguiente', categoria: 'Informativa mensual' },
|
||||
{ id: 'imss-cuotas', nombre: 'Cuotas obrero-patronales IMSS', fundamento: 'LSS', frecuencia: 'mensual', fechaLimite: 'Día 17 del mes siguiente', categoria: 'Seguridad social' },
|
||||
{ id: 'anual-isr-pm', nombre: 'Declaración Anual de ISR PM', fundamento: 'Art. 76 LISR', frecuencia: 'anual', fechaLimite: '31 de marzo', categoria: 'Anual' },
|
||||
{ id: 'isn', nombre: 'ISN - Impuesto Sobre Nómina', fundamento: 'Ley estatal', frecuencia: 'mensual', fechaLimite: 'Varía por estado (CDMX día 17)', categoria: 'Estatal' },
|
||||
{ id: 'isrtp', nombre: 'Impuesto sobre remuneración al trabajo', fundamento: 'Ley estatal', frecuencia: 'mensual', fechaLimite: 'Día 10 del mes siguiente', categoria: 'Estatal' },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Sembrando obligaciones y tareas en Demo Ventas...\n');
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO_RFC } });
|
||||
if (!tenant) throw new Error(`Tenant ${DEMO_RFC} no encontrado`);
|
||||
|
||||
const auxUser = await prisma.user.findUnique({ where: { email: 'auxiliar@horuxfin.com' } });
|
||||
if (!auxUser) throw new Error('Usuario auxiliar no encontrado');
|
||||
|
||||
const supervisorUser = await prisma.user.findUnique({ where: { email: 'supervisor@horuxfin.com' } });
|
||||
if (!supervisorUser) throw new Error('Usuario supervisor no encontrado');
|
||||
|
||||
const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
|
||||
|
||||
const { rows: contribuyentes } = await pool.query<{ id: string; rfc: string }>(`
|
||||
SELECT entidad_id AS id, rfc FROM contribuyentes ORDER BY rfc
|
||||
`);
|
||||
|
||||
if (contribuyentes.length === 0) throw new Error('No hay contribuyentes en el tenant demo');
|
||||
|
||||
for (const c of contribuyentes) {
|
||||
// Obligaciones fiscales (idempotente: evita duplicados por contribuyente + catalogo_id)
|
||||
let obligacionesCreadas = 0;
|
||||
for (const o of OBLIGACIONES) {
|
||||
const { rows: existing } = await pool.query(
|
||||
`SELECT 1 FROM obligaciones_contribuyente WHERE contribuyente_id = $1 AND catalogo_id = $2 LIMIT 1`,
|
||||
[c.id, o.id],
|
||||
);
|
||||
if (existing.length > 0) continue;
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO obligaciones_contribuyente (
|
||||
contribuyente_id, catalogo_id, nombre, fundamento, frecuencia, fecha_limite, categoria, activa, es_recomendada
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, true, true)
|
||||
`, [c.id, o.id, o.nombre, o.fundamento, o.frecuencia, o.fechaLimite, o.categoria]);
|
||||
obligacionesCreadas++;
|
||||
}
|
||||
console.log(`✅ ${c.rfc}: ${obligacionesCreadas} obligaciones creadas`);
|
||||
|
||||
// Tareas default
|
||||
const tareasCreadas = await seedTareasDefault(pool, c.id);
|
||||
if (tareasCreadas > 0) {
|
||||
await materializarPeriodos(pool, c.id);
|
||||
console.log(`✅ ${c.rfc}: ${tareasCreadas} tareas creadas y periodos materializados`);
|
||||
} else {
|
||||
console.log(`ℹ️ ${c.rfc}: tareas default ya existían`);
|
||||
}
|
||||
}
|
||||
|
||||
// Asignar auxiliar a todas las obligaciones y tareas activas
|
||||
await pool.query(`
|
||||
INSERT INTO obligacion_asignaciones (obligacion_id, auxiliar_user_id, asignado_por)
|
||||
SELECT oc.id, $1, $2
|
||||
FROM obligaciones_contribuyente oc
|
||||
WHERE oc.activa = true
|
||||
ON CONFLICT (obligacion_id) DO UPDATE SET auxiliar_user_id = EXCLUDED.auxiliar_user_id, asignado_por = EXCLUDED.asignado_por
|
||||
`, [auxUser.id, supervisorUser.id]);
|
||||
console.log('✅ Auxiliar asignado a obligaciones');
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO tarea_asignaciones (tarea_id, auxiliar_user_id, asignado_por)
|
||||
SELECT tc.id, $1, $2
|
||||
FROM tareas_catalogo tc
|
||||
WHERE tc.active = true
|
||||
ON CONFLICT (tarea_id) DO UPDATE SET auxiliar_user_id = EXCLUDED.auxiliar_user_id, asignado_por = EXCLUDED.asignado_por
|
||||
`, [auxUser.id, supervisorUser.id]);
|
||||
console.log('✅ Auxiliar asignado a tareas');
|
||||
|
||||
// Asignar auxiliar a la cartera principal
|
||||
await pool.query(`
|
||||
UPDATE carteras SET auxiliar_user_id = $1
|
||||
WHERE parent_id IS NULL
|
||||
`, [auxUser.id]);
|
||||
console.log('✅ Auxiliar asignado a la cartera principal');
|
||||
|
||||
// Asegurar relación auxiliar-supervisor
|
||||
await pool.query(`
|
||||
INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = EXCLUDED.supervisor_user_id
|
||||
`, [auxUser.id, supervisorUser.id]);
|
||||
console.log('✅ Relación auxiliar → supervisor registrada');
|
||||
|
||||
console.log('\n🎉 Obligaciones y tareas listas en Demo Ventas');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await tenantDb.shutdown();
|
||||
});
|
||||
337
apps/api/scripts/update-demo-ventas.ts
Normal file
337
apps/api/scripts/update-demo-ventas.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Script: update-demo-ventas
|
||||
*
|
||||
* Agrega al tenant Demo Ventas:
|
||||
* - 5 contribuyentes adicionales
|
||||
* - Usuarios supervisor, auxiliar y cliente con sus memberships
|
||||
* - CFDIs de ejemplo para los nuevos contribuyentes
|
||||
* - Accesos de cliente a los contribuyentes
|
||||
* - Ajusta el plan custom para soportar más RFCs/usuarios
|
||||
*
|
||||
* Ejecución:
|
||||
* cd apps/api && npx tsx scripts/update-demo-ventas.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Pool } from 'pg';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { tenantDb } from '../src/config/database.ts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const DEMO_RFC = 'DEMO2501019X2';
|
||||
const DEFAULT_PASSWORD = 'Demo12345!';
|
||||
|
||||
const NUEVOS_CONTRIBUYENTES = [
|
||||
{ rfc: 'COM2501019X1', nombre: 'Comercial del Norte SA de CV', cp: '64000' },
|
||||
{ rfc: 'DIS2501019X1', nombre: 'Distribuidora del Centro SA de CV', cp: '44100' },
|
||||
{ rfc: 'SIS2501019X1', nombre: 'Servicios Integrales del Sur SA de CV', cp: '86000' },
|
||||
{ rfc: 'IMP2501019X1', nombre: 'Importadora del Pacifico SA de CV', cp: '82140' },
|
||||
{ rfc: 'EXA2501019X1', nombre: 'Exportadora del Atlantico SA de CV', cp: '94270' },
|
||||
];
|
||||
|
||||
const USUARIOS = [
|
||||
{ email: 'supervisor@horuxfin.com', nombre: 'Supervisor Demo', rol: 'supervisor' },
|
||||
{ email: 'auxiliar@horuxfin.com', nombre: 'Auxiliar Demo', rol: 'auxiliar' },
|
||||
{ email: 'cliente@horuxfin.com', nombre: 'Cliente Demo', rol: 'cliente' },
|
||||
];
|
||||
|
||||
const CLIENTES = [
|
||||
{ rfc: 'CLI123456AB1', nombre: 'Cliente Alfa SA' },
|
||||
{ rfc: 'CLI123456AB2', nombre: 'Cliente Beta SA' },
|
||||
{ rfc: 'CLI123456AB3', nombre: 'Cliente Gamma SA' },
|
||||
];
|
||||
|
||||
const PROVEEDORES = [
|
||||
{ rfc: 'PRO123456AB1', nombre: 'Proveedor Materiales SA' },
|
||||
{ rfc: 'PRO123456AB2', nombre: 'Proveedor Servicios SA' },
|
||||
{ rfc: 'PRO123456AB3', nombre: 'Proveedor Logistica SA' },
|
||||
];
|
||||
|
||||
const PRODUCTOS = [
|
||||
{ clave: '84111506', descripcion: 'Servicio de consultoria', unidad: 'Servicio' },
|
||||
{ clave: '43232408', descripcion: 'Licencia de software', unidad: 'Licencia' },
|
||||
{ clave: '81141500', descripcion: 'Soporte tecnico', unidad: 'Servicio' },
|
||||
{ clave: '81121700', descripcion: 'Desarrollo web', unidad: 'Servicio' },
|
||||
{ clave: '86101500', descripcion: 'Capacitacion', unidad: 'Servicio' },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Actualizando Demo Ventas...\n');
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({ where: { rfc: DEMO_RFC } });
|
||||
if (!tenant) throw new Error(`Tenant ${DEMO_RFC} no encontrado`);
|
||||
|
||||
// Ajustar catálogo del plan custom para soportar la demo completa
|
||||
await prisma.despachoPlanPrice.update({
|
||||
where: { plan: 'custom' },
|
||||
data: { maxRfcs: 10, maxUsers: 10 },
|
||||
});
|
||||
console.log('✅ Plan custom actualizado: maxRfcs=10, maxUsers=10');
|
||||
|
||||
// Crear/actualizar usuarios y memberships
|
||||
const createdUsers: Record<string, { id: string; rolId: number }> = {};
|
||||
for (const u of USUARIOS) {
|
||||
const rol = await prisma.rol.findUnique({ where: { nombre: u.rol } });
|
||||
if (!rol) throw new Error(`Rol ${u.rol} no encontrado`);
|
||||
|
||||
let user = await prisma.user.findUnique({ where: { email: u.email } });
|
||||
const passwordHash = await bcrypt.hash(DEFAULT_PASSWORD, 12);
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
data: { email: u.email, passwordHash, nombre: u.nombre, lastTenantId: tenant.id },
|
||||
});
|
||||
} else {
|
||||
user = await prisma.user.update({ where: { id: user.id }, data: { passwordHash, lastTenantId: tenant.id } });
|
||||
}
|
||||
|
||||
await prisma.tenantMembership.upsert({
|
||||
where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } },
|
||||
update: { rolId: rol.id, active: true, isOwner: false },
|
||||
create: { userId: user.id, tenantId: tenant.id, rolId: rol.id, active: true, isOwner: false },
|
||||
});
|
||||
|
||||
createdUsers[u.rol] = { id: user.id, rolId: rol.id };
|
||||
console.log(`✅ Usuario ${u.rol}:`, u.email);
|
||||
}
|
||||
|
||||
const supervisorId = createdUsers.supervisor.id;
|
||||
const clienteId = createdUsers.cliente.id;
|
||||
|
||||
// Conectar a BD del tenant
|
||||
const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
|
||||
|
||||
// Crear contribuyentes, CFDIs y accesos
|
||||
const contribuyenteIds: string[] = [];
|
||||
for (const c of NUEVOS_CONTRIBUYENTES) {
|
||||
const id = await crearContribuyente(pool, c, supervisorId, tenant.id);
|
||||
contribuyenteIds.push(id);
|
||||
console.log(`✅ Contribuyente creado: ${c.rfc}`);
|
||||
|
||||
await crearCfdis(pool, id, c.rfc, c.nombre);
|
||||
}
|
||||
|
||||
// Asignar accesos de cliente a todos los contribuyentes (incluido el original)
|
||||
const { rows: todasEntidades } = await pool.query<{ id: string }>(`
|
||||
SELECT entidad_id AS id FROM contribuyentes
|
||||
`);
|
||||
for (const e of todasEntidades) {
|
||||
await pool.query(`
|
||||
INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [clienteId, e.id]);
|
||||
}
|
||||
console.log('✅ Accesos de cliente asignados a', todasEntidades.length, 'contribuyentes');
|
||||
|
||||
// Agregar nuevos contribuyentes a la cartera principal
|
||||
const { rows: [cartera] } = await pool.query<{ id: string }>(`
|
||||
SELECT id FROM carteras ORDER BY created_at LIMIT 1
|
||||
`);
|
||||
if (cartera) {
|
||||
for (const id of contribuyenteIds) {
|
||||
await pool.query(`
|
||||
INSERT INTO cartera_entidades (cartera_id, entidad_id) VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, [cartera.id, id]);
|
||||
}
|
||||
console.log('✅ Nuevos contribuyentes agregados a cartera principal');
|
||||
}
|
||||
|
||||
console.log('\n🎉 Demo Ventas actualizada');
|
||||
console.log(' Nuevos contribuyentes:', NUEVOS_CONTRIBUYENTES.length);
|
||||
console.log(' Usuos adicionales:');
|
||||
for (const u of USUARIOS) {
|
||||
console.log(` ${u.rol}: ${u.email} / ${DEFAULT_PASSWORD}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function crearContribuyente(pool: Pool, data: { rfc: string; nombre: string; cp: string }, supervisorId: string, tenantId: string): Promise<string> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Buscar si ya existe la entidad para este RFC
|
||||
const { rows: existingEntidad } = await client.query<{ id: string }>(`
|
||||
SELECT e.id FROM entidades_gestionadas e
|
||||
WHERE e.identificador = $1 AND e.tipo = 'CONTRIBUYENTE'
|
||||
`, [data.rfc]);
|
||||
|
||||
let entidadId: string;
|
||||
if (existingEntidad.length > 0) {
|
||||
entidadId = existingEntidad[0].id;
|
||||
await client.query(`
|
||||
UPDATE entidades_gestionadas
|
||||
SET nombre = $1, supervisor_user_id = $2, updated_at = now()
|
||||
WHERE id = $3
|
||||
`, [data.nombre, supervisorId, entidadId]);
|
||||
} else {
|
||||
const { rows: [entidad] } = await client.query<{ id: string }>(`
|
||||
INSERT INTO entidades_gestionadas (tipo, nombre, identificador, supervisor_user_id)
|
||||
VALUES ('CONTRIBUYENTE', $1, $2, $3)
|
||||
RETURNING id
|
||||
`, [data.nombre, data.rfc, supervisorId]);
|
||||
entidadId = entidad.id;
|
||||
}
|
||||
|
||||
const { rows: existingContrib } = await client.query<{ entidad_id: string }>(`
|
||||
SELECT entidad_id FROM contribuyentes WHERE entidad_id = $1
|
||||
`, [entidadId]);
|
||||
|
||||
if (existingContrib.length > 0) {
|
||||
await client.query(`
|
||||
UPDATE contribuyentes
|
||||
SET rfc = $1, regimen_fiscal = $2, codigo_postal = $3
|
||||
WHERE entidad_id = $4
|
||||
`, [data.rfc, '601', data.cp, entidadId]);
|
||||
} else {
|
||||
await client.query(`
|
||||
INSERT INTO contribuyentes (entidad_id, rfc, regimen_fiscal, codigo_postal)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [entidadId, data.rfc, '601', data.cp]);
|
||||
}
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO rfcs (rfc, razon_social, regimen_fiscal)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social
|
||||
`, [data.rfc, data.nombre, '601']);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return entidadId;
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function crearCfdis(pool: Pool, contribuyenteId: string, rfcContribuyente: string, nombreContribuyente: string) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Asegurar RFCs de clientes/proveedores
|
||||
const rfcs = new Map<string, number>();
|
||||
for (const c of [...CLIENTES, ...PROVEEDORES]) {
|
||||
const { rows: [r] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO rfcs (rfc, razon_social, regimen_fiscal)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social
|
||||
RETURNING id
|
||||
`, [c.rfc, c.nombre, '601']);
|
||||
rfcs.set(c.rfc, r.id);
|
||||
}
|
||||
const { rows: [rfcPrincipal] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO rfcs (rfc, razon_social, regimen_fiscal)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (rfc) DO UPDATE SET razon_social = EXCLUDED.razon_social
|
||||
RETURNING id
|
||||
`, [rfcContribuyente, nombreContribuyente, '601']);
|
||||
rfcs.set(rfcContribuyente, rfcPrincipal.id);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const esEmitido = i < 5;
|
||||
const contraparte = esEmitido
|
||||
? CLIENTES[i % CLIENTES.length]
|
||||
: PROVEEDORES[i % PROVEEDORES.length];
|
||||
|
||||
const subtotal = Math.floor(Math.random() * 30000) + 1500;
|
||||
const iva = Math.round(subtotal * 0.16 * 100) / 100;
|
||||
const total = Math.round((subtotal + iva) * 100) / 100;
|
||||
|
||||
const daysAgo = Math.floor(Math.random() * 360);
|
||||
const fecha = new Date();
|
||||
fecha.setDate(fecha.getDate() - daysAgo);
|
||||
fecha.setHours(9 + (i % 8), 0, 0, 0);
|
||||
|
||||
const year = String(fecha.getFullYear());
|
||||
const month = String(fecha.getMonth() + 1).padStart(2, '0');
|
||||
const fechaStr = fecha.toISOString();
|
||||
const metodoPago = Math.random() > 0.4 ? 'PUE' : 'PPD';
|
||||
const formasPago = ['01', '02', '03'];
|
||||
const formaPago = formasPago[i % formasPago.length];
|
||||
const usoCfdi = esEmitido ? 'G03' : 'G01';
|
||||
const tipo = esEmitido ? 'EMITIDO' : 'RECIBIDO';
|
||||
|
||||
const rfcEmisor = esEmitido ? rfcContribuyente : contraparte.rfc;
|
||||
const nombreEmisor = esEmitido ? nombreContribuyente : contraparte.nombre;
|
||||
const rfcReceptor = esEmitido ? contraparte.rfc : rfcContribuyente;
|
||||
const nombreReceptor = esEmitido ? contraparte.nombre : nombreContribuyente;
|
||||
|
||||
const { rows: [cfdi] } = await client.query<{ id: number }>(`
|
||||
INSERT INTO cfdis (
|
||||
year, month, type, uuid, serie, folio, status, fecha_emision,
|
||||
rfc_emisor_id, rfc_emisor, nombre_emisor,
|
||||
rfc_receptor_id, rfc_receptor, nombre_receptor,
|
||||
subtotal, subtotal_mxn, descuento, descuento_mxn,
|
||||
total, total_mxn, moneda, tipo_cambio, tipo_comprobante,
|
||||
metodo_pago, forma_pago, uso_cfdi,
|
||||
iva_traslado, iva_traslado_mxn,
|
||||
regimen_fiscal_emisor, regimen_fiscal_receptor,
|
||||
contribuyente_id, fecha_efectiva, meses_global, año_global
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
$9, $10, $11,
|
||||
$12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20, $21, $22, $23,
|
||||
$24, $25, $26,
|
||||
$27, $28,
|
||||
$29, $30,
|
||||
$31, $32, $33, $34
|
||||
) RETURNING id
|
||||
`, [
|
||||
year, month, tipo, randomUUID(), 'DEMO', String(2000 + i),
|
||||
'Vigente', fechaStr,
|
||||
rfcs.get(rfcEmisor), rfcEmisor, nombreEmisor,
|
||||
rfcs.get(rfcReceptor), rfcReceptor, nombreReceptor,
|
||||
subtotal, subtotal, 0, 0,
|
||||
total, total, 'MXN', 1, 'I',
|
||||
metodoPago, formaPago, usoCfdi,
|
||||
iva, iva,
|
||||
'601', '601',
|
||||
contribuyenteId, fechaStr, month, year,
|
||||
]);
|
||||
|
||||
const numConceptos = Math.floor(Math.random() * 2) + 1;
|
||||
for (let j = 0; j < numConceptos; j++) {
|
||||
const prod = PRODUCTOS[(i + j) % PRODUCTOS.length];
|
||||
const cantidad = Math.floor(Math.random() * 4) + 1;
|
||||
const valorUnitario = Math.floor(Math.random() * 3000) + 500;
|
||||
const importe = Math.round(cantidad * valorUnitario * 100) / 100;
|
||||
const ivaConcepto = Math.round(importe * 0.16 * 100) / 100;
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO cfdi_conceptos (
|
||||
cfdi_id, clave_prod_serv, descripcion, cantidad, clave_unidad, unidad,
|
||||
valor_unitario, valor_unitario_mxn, importe, importe_mxn,
|
||||
iva_traslado, iva_traslado_mxn
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
`, [
|
||||
cfdi.id, prod.clave, prod.descripcion, cantidad, 'E48', prod.unidad,
|
||||
valorUnitario, valorUnitario, importe, importe,
|
||||
ivaConcepto, ivaConcepto,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(` 📄 10 CFDIs creados para ${rfcContribuyente}`);
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('\n❌ Error actualizando demo:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await tenantDb.shutdown();
|
||||
});
|
||||
Reference in New Issue
Block a user