Files
HoruxDespachosNuevo/apps/api/scripts/fix-null-cfdi-fields.ts
Horux Dev 099f34c903 feat(sat): reporte diario de errores SAT por proxy
- Agrega tabla sat_proxy_errors con proxy_usado, error_code, stage, message.
- sat-client.service.ts expone proxyInfo usado en cada conexión SAT.
- sat.service.ts registra errores de bloqueo/devolución del SAT en sat_proxy_errors
  y guarda proxyUsed en sat_sync_jobs.
- Nuevo job sat-proxy-report.job.ts: cron 8 AM CDMX, envía email a ADMIN_EMAIL
  con errores de las últimas 24h agrupados por proxy/error_code.
- Template de email sat-proxy-report.ts y método sendSatProxyReport.
- Registra el cron en src/index.ts.
- Actualiza docs/SAT-SYNC-IMPLEMENTATION.md.
2026-08-03 14:27:19 +00:00

151 lines
4.9 KiB
TypeScript

/**
* Script para reparar CFDIs tipo I/E que tienen XML pero campos nulos
* (subtotal, pac, tipo_cambio, impuestos, etc.)
*
* El problema: algunos CFDIs fueron insertados por saveMetadata y luego
* saveCfdis no actualizó todos los campos correctamente.
*/
import { Pool } from 'pg';
import { parseXml } from '../src/services/sat/sat-parser.service';
const DB_CONFIG = {
host: 'localhost',
user: 'postgres',
password: 'ZxHMrmnwanvLfLDdNJdRthFjWF2Lj1Rb',
};
const TENANTS = [
{ db: 'horux_auza640701ti9', rfc: 'AUZA640701TI9' },
{ db: 'horux_hts240708lja', rfc: 'HTS240708LJA' },
{ db: 'horux_momc8311199va', rfc: 'MOMC8311199VA' },
{ db: 'horux_roem691011ez4', rfc: 'ROEM691011EZ4' },
{ db: 'horux_toah680201ra2', rfc: 'TOAH680201RA2' },
{ db: 'horux_tora0007099r6', rfc: 'TORA0007099R6' },
{ db: 'horux_despacho_mpg95qp7_xzvff', rfc: 'MPG95QP7_XZVFF' },
];
async function fixTenant(dbName: string, rfc: string): Promise<{ fixed: number; errors: number }> {
const pool = new Pool({ ...DB_CONFIG, database: dbName });
let fixed = 0;
let errors = 0;
try {
// CFDIs tipo I/E con XML pero sin subtotal (indica que saveCfdis no actualizó bien)
const { rows } = await pool.query(`
SELECT id, uuid, xml_original, type
FROM cfdis
WHERE tipo_comprobante IN ('I', 'E')
AND xml_original IS NOT NULL AND xml_original <> ''
AND subtotal IS NULL
`);
console.log(`[${rfc}] ${rows.length} CFDIs a reparar`);
for (const row of rows) {
try {
const downloadType = row.type === 'EMITIDO' ? 'emitidos' : 'recibidos';
const parsed = parseXml(row.xml_original, downloadType);
if (!parsed) {
console.warn(`[${rfc}] No se pudo parsear UUID ${row.uuid}`);
errors++;
continue;
}
const tc = parsed.tipoCambio || 1;
const m = (v: number) => v * tc;
await pool.query(`
UPDATE cfdis SET
subtotal = $1,
subtotal_mxn = $2,
descuento = $3,
descuento_mxn = $4,
total = $5,
total_mxn = $6,
moneda = $7,
tipo_cambio = $8,
metodo_pago = $9,
forma_pago = $10,
uso_cfdi = $11,
pac = $12,
fecha_cert_sat = $13,
uuid_relacionado = $14,
isr_retencion = $15,
isr_retencion_mxn = $16,
iva_traslado = $17,
iva_traslado_mxn = $18,
iva_retencion = $19,
iva_retencion_mxn = $20,
ieps_traslado = $21,
ieps_traslado_mxn = $22,
ieps_retencion = $23,
ieps_retencion_mxn = $24,
impuestos_locales_trasladado = $25,
impuestos_locales_trasladado_mxn = $26,
impuestos_locales_retenidos = $27,
impuestos_locales_retenidos_mxn = $28,
regimen_fiscal_emisor = $29,
regimen_fiscal_receptor = $30,
codigo_postal_receptor = $31,
serie = $32,
folio = $33,
cfdi_tipo_relacion = $34,
cfdis_relacionados = $35,
actualizado_en = NOW()
WHERE id = $36
`, [
parsed.subtotal, m(parsed.subtotal),
parsed.descuento, m(parsed.descuento),
parsed.total, m(parsed.total),
parsed.moneda, tc,
parsed.metodoPago, parsed.formaPago, parsed.usoCfdi,
parsed.pac, parsed.fechaCertSat,
parsed.uuidRelacionado,
parsed.isrRetencion, m(parsed.isrRetencion),
parsed.ivaTraslado, m(parsed.ivaTraslado),
parsed.ivaRetencion, m(parsed.ivaRetencion),
parsed.iepsTraslado, m(parsed.iepsTraslado),
parsed.iepsRetencion, m(parsed.iepsRetencion),
parsed.impuestosLocalesTrasladado, m(parsed.impuestosLocalesTrasladado),
parsed.impuestosLocalesRetenidos, m(parsed.impuestosLocalesRetenidos),
parsed.regimenFiscalEmisor, parsed.regimenFiscalReceptor,
parsed.codigoPostalReceptor,
parsed.serie, parsed.folio,
parsed.cfdiTipoRelacion, parsed.cfdisRelacionados,
row.id,
]);
fixed++;
} catch (err: any) {
console.error(`[${rfc}] Error reparando UUID ${row.uuid}:`, err.message);
errors++;
}
}
} finally {
await pool.end();
}
return { fixed, errors };
}
async function main() {
let totalFixed = 0;
let totalErrors = 0;
for (const tenant of TENANTS) {
try {
const result = await fixTenant(tenant.db, tenant.rfc);
totalFixed += result.fixed;
totalErrors += result.errors;
console.log(`[${tenant.rfc}] Reparados: ${result.fixed}, Errores: ${result.errors}`);
} catch (err: any) {
console.error(`[${tenant.rfc}] Error general:`, err.message);
totalErrors++;
}
}
console.log(`\n=== TOTAL: Reparados ${totalFixed}, Errores ${totalErrors} ===`);
}
main().catch(console.error);