feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos

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

Frontend:
- Nueva página /admin/facturas-pendientes con tabla y emisión manual
- Métrica 'Facturas pendientes' en /clientes (clickable)
- Navegación onboarding FIEL/CSD corregida
- Sidebar themes sincronizados
- Fix SAT portal migration scraper (NetIQ)
- Trial invitation acceptance pages
This commit is contained in:
Horux Dev
2026-05-09 21:56:42 +00:00
parent b00b677c54
commit 9f11a0ba39
70 changed files with 2801 additions and 609 deletions

187
scripts/reprocess_bom.js Normal file
View File

@@ -0,0 +1,187 @@
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');
const { parseXml } = require('/root/HoruxDespachosNuevo/apps/api/dist/services/sat/sat-parser.service.js');
const DB_PASSWORD = 'ZxHMrmnwanvLfLDdNJdRthFjWF2Lj1Rb';
const BASE_DIR = '/root/HoruxDespachosNuevo/apps/api/data/xmls';
function getPool(dbName) {
return new Pool({ host: 'localhost', user: 'postgres', password: DB_PASSWORD, database: dbName });
}
async function reprocessXml(filePath, rfc, tipoCfdi) {
let xmlContent = fs.readFileSync(filePath, 'utf-8');
if (xmlContent.charCodeAt(0) === 0xFEFF) {
xmlContent = xmlContent.slice(1);
}
const cfdi = parseXml(xmlContent, tipoCfdi);
if (!cfdi) {
console.log(` SKIP: parseXml returned null for ${filePath}`);
return false;
}
const dbName = `horux_${rfc.toLowerCase()}`;
const pool = getPool(dbName);
try {
const uuidNorm = cfdi.uuid.toLowerCase();
const { rows: existing } = await pool.query(
`SELECT id FROM cfdis WHERE LOWER(uuid) = $1`, [uuidNorm]
);
if (existing.length === 0) {
console.log(` SKIP: CFDI ${uuidNorm} not found in DB ${dbName}`);
return false;
}
const cfdiId = existing[0].id;
const tc = cfdi.tipoCambio || 1;
const m = (v) => (v || 0) * tc;
// Update cfdis
await pool.query(
`UPDATE cfdis SET
serie = $1, folio = $2, status = $3, fecha_emision = $4, fecha_cert_sat = $5,
rfc_emisor = $6, nombre_emisor = $7, rfc_receptor = $8, nombre_receptor = $9,
subtotal = $10, subtotal_mxn = $11, descuento = $12, descuento_mxn = $13,
total = $14, total_mxn = $15, moneda = $16, tipo_cambio = $17,
metodo_pago = $18, forma_pago = $19, uso_cfdi = $20, pac = $21,
isr_retencion = $22, isr_retencion_mxn = $23,
iva_traslado = $24, iva_traslado_mxn = $25,
iva_retencion = $26, iva_retencion_mxn = $27,
ieps_traslado = $28, ieps_traslado_mxn = $29,
ieps_retencion = $30, ieps_retencion_mxn = $31,
impuestos_locales_trasladado = $32, impuestos_locales_trasladado_mxn = $33,
impuestos_locales_retenidos = $34, impuestos_locales_retenidos_mxn = $35,
monto_pago = $36, monto_pago_mxn = $37,
fecha_pago_p = $38, num_parcialidad = $39,
isr_retencion_pago = $40, isr_retencion_pago_mxn = $41,
iva_traslado_pago = $42, iva_traslado_pago_mxn = $43,
iva_retencion_pago = $44, iva_retencion_pago_mxn = $45,
ieps_traslado_pago = $46, ieps_traslado_pago_mxn = $47,
ieps_retencion_pago = $48, ieps_retencion_pago_mxn = $49,
fecha_pago = $50, fecha_inicial_pago = $51, fecha_final_pago = $52,
num_dias_pagados = $53, num_seguro_social = $54, puesto = $55,
salario_base_cot_apor = $56, salario_base_cot_apor_mxn = $57,
salario_diario_integrado = $58, salario_diario_integrado_mxn = $59,
total_percepciones = $60, total_percepciones_mxn = $61,
total_deducciones = $62, total_deducciones_mxn = $63,
imp_retenidos_nomina = $64, imp_retenidos_nomina_mxn = $65,
otras_deducciones_nomina = $66, otras_deducciones_nomina_mxn = $67,
subsidio_causado = $68, subsidio_causado_mxn = $69,
regimen_fiscal_emisor = $70, regimen_fiscal_receptor = $71,
xml_original = $72, cfdi_tipo_relacion = $73, cfdis_relacionados = $74,
saldo_insoluto = $75, uuid_relacionado = $76,
actualizado_en = NOW()
WHERE id = $77`,
[
cfdi.serie, cfdi.folio, cfdi.status, cfdi.fechaEmision, cfdi.fechaCertSat,
cfdi.rfcEmisor, cfdi.nombreEmisor, cfdi.rfcReceptor, cfdi.nombreReceptor,
cfdi.subtotal, m(cfdi.subtotal), cfdi.descuento, m(cfdi.descuento),
cfdi.total, m(cfdi.total), cfdi.moneda, cfdi.tipoCambio,
cfdi.metodoPago, cfdi.formaPago, cfdi.usoCfdi, cfdi.pac,
cfdi.isrRetencion, m(cfdi.isrRetencion),
cfdi.ivaTraslado, m(cfdi.ivaTraslado),
cfdi.ivaRetencion, m(cfdi.ivaRetencion),
cfdi.iepsTraslado, m(cfdi.iepsTraslado),
cfdi.iepsRetencion, m(cfdi.iepsRetencion),
cfdi.impuestosLocalesTrasladado, m(cfdi.impuestosLocalesTrasladado),
cfdi.impuestosLocalesRetenidos, m(cfdi.impuestosLocalesRetenidos),
cfdi.montoPago, m(cfdi.montoPago),
cfdi.fechaPagoP, cfdi.numParcialidad,
cfdi.isrRetencionPago, m(cfdi.isrRetencionPago),
cfdi.ivaTrasladoPago, m(cfdi.ivaTrasladoPago),
cfdi.ivaRetencionPago, m(cfdi.ivaRetencionPago),
cfdi.iepsTrasladoPago, m(cfdi.iepsTrasladoPago),
cfdi.iepsRetencionPago, m(cfdi.iepsRetencionPago),
cfdi.fechaPago, cfdi.fechaInicialPago, cfdi.fechaFinalPago,
cfdi.numDiasPagados, cfdi.numSeguroSocial, cfdi.puesto,
cfdi.salarioBaseCotApor, m(cfdi.salarioBaseCotApor),
cfdi.salarioDiarioIntegrado, m(cfdi.salarioDiarioIntegrado),
cfdi.totalPercepciones, m(cfdi.totalPercepciones),
cfdi.totalDeducciones, m(cfdi.totalDeducciones),
cfdi.impRetenidosNomina, m(cfdi.impRetenidosNomina),
cfdi.otrasDeduccionesNomina, m(cfdi.otrasDeduccionesNomina),
cfdi.subsidioCausado, m(cfdi.subsidioCausado),
cfdi.regimenFiscalEmisor, cfdi.regimenFiscalReceptor,
xmlContent, cfdi.cfdiTipoRelacion, cfdi.cfdisRelacionados,
cfdi.saldoInsoluto, cfdi.uuidRelacionado,
cfdiId
]
);
// Re-insert conceptos
await pool.query(`DELETE FROM cfdi_conceptos WHERE cfdi_id = $1`, [cfdiId]);
for (const c of cfdi.conceptos || []) {
await pool.query(
`INSERT INTO cfdi_conceptos (
cfdi_id, clave_prod_serv, no_identificacion, descripcion, cantidad,
clave_unidad, unidad, valor_unitario, valor_unitario_mxn, importe, importe_mxn,
descuento, descuento_mxn,
isr_retencion, isr_retencion_mxn, iva_traslado, iva_traslado_mxn,
iva_retencion, iva_retencion_mxn, ieps_traslado, ieps_traslado_mxn,
ieps_retencion, ieps_retencion_mxn
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23)`,
[
cfdiId, c.claveProdServ, c.noIdentificacion, c.descripcion, c.cantidad,
c.claveUnidad, c.unidad, c.valorUnitario, m(c.valorUnitario), c.importe, m(c.importe),
c.descuento, m(c.descuento),
c.isrRetencion, m(c.isrRetencion), c.ivaTraslado, m(c.ivaTraslado),
c.ivaRetencion, m(c.ivaRetencion), c.iepsTraslado, m(c.iepsTraslado),
c.iepsRetencion, m(c.iepsRetencion)
]
);
}
console.log(` OK: ${uuidNorm} updated in ${dbName}`);
return true;
} catch (err) {
console.error(` ERROR: ${filePath} - ${err.message}`);
return false;
} finally {
await pool.end();
}
}
async function main() {
let processed = 0;
let skipped = 0;
let errors = 0;
const rfcs = fs.readdirSync(BASE_DIR).filter(d => fs.statSync(path.join(BASE_DIR, d)).isDirectory());
for (const rfc of rfcs) {
const rfcDir = path.join(BASE_DIR, rfc);
const tipos = fs.readdirSync(rfcDir).filter(d => fs.statSync(path.join(rfcDir, d)).isDirectory());
for (const tipo of tipos) {
const tipoDir = path.join(rfcDir, tipo);
const packages = fs.readdirSync(tipoDir).filter(d => fs.statSync(path.join(tipoDir, d)).isDirectory());
for (const pkg of packages) {
const pkgDir = path.join(tipoDir, pkg);
const files = fs.readdirSync(pkgDir).filter(f => f.endsWith('.xml'));
for (const file of files) {
const filePath = path.join(pkgDir, file);
const buf = fs.readFileSync(filePath);
if (buf.length < 3 || !(buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF)) {
continue;
}
console.log(`Processing ${filePath}...`);
const ok = await reprocessXml(filePath, rfc, tipo);
if (ok) processed++;
else skipped++;
if (!ok) errors++;
}
}
}
}
console.log(`\nDone. Processed: ${processed}, Skipped/Errors: ${errors}`);
}
main().catch(console.error);