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:
187
scripts/reprocess_bom.js
Normal file
187
scripts/reprocess_bom.js
Normal 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);
|
||||
Reference in New Issue
Block a user