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.
This commit is contained in:
150
apps/api/scripts/fix-null-cfdi-fields.ts
Normal file
150
apps/api/scripts/fix-null-cfdi-fields.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user