- 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.
338 lines
13 KiB
TypeScript
338 lines
13 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|