Update: nueva version Horux Despachos
This commit is contained in:
174
apps/api/scripts/backfill-fechas-tz.ts
Normal file
174
apps/api/scripts/backfill-fechas-tz.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Backfill de `fecha_emision` (y opcionalmente `fecha_cert_sat`) para CFDIs
|
||||
* sincronizados antes del fix de zona horaria. El parser convertía la fecha
|
||||
* del XML ("2025-12-31T18:37:51") asumiéndola como hora local de la máquina
|
||||
* y la guardaba en UTC ("2026-01-01T00:37:51Z"), corriendo 6 horas y a veces
|
||||
* sacando el CFDI de su mes/año correcto.
|
||||
*
|
||||
* Re-parsea la fecha literal del XML (atributo `Fecha=""` del Comprobante y
|
||||
* `FechaTimbrado=""` del TimbreFiscalDigital) y lo guarda como UTC-literal
|
||||
* (forzando 'Z' al string del XML).
|
||||
*
|
||||
* Solo aplica a CFDIs con `xml_original IS NOT NULL`. Idempotente.
|
||||
*
|
||||
* Uso:
|
||||
* pnpm --filter @horux/api exec tsx scripts/backfill-fechas-tz.ts # ejecuta
|
||||
* pnpm --filter @horux/api exec tsx scripts/backfill-fechas-tz.ts --dry # reporta
|
||||
*/
|
||||
import { prisma, tenantDb } from '../src/config/database.js';
|
||||
|
||||
const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run');
|
||||
|
||||
function parseLiteral(str: string | null | undefined): Date | null {
|
||||
if (!str) return null;
|
||||
const s = String(str).trim();
|
||||
if (!s) return null;
|
||||
const hasTz = /[Zz]|[+-]\d{2}:?\d{2}$/.test(s);
|
||||
return new Date(hasTz ? s : s + 'Z');
|
||||
}
|
||||
|
||||
function extractFechaFromXml(xml: string): string | null {
|
||||
// Atributo Fecha del root <cfdi:Comprobante Fecha="...">
|
||||
const m = xml.match(/<cfdi:Comprobante\b[^>]*\bFecha="([^"]+)"/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
function extractFechaTimbradoFromXml(xml: string): string | null {
|
||||
const m = xml.match(/<tfd:TimbreFiscalDigital\b[^>]*\bFechaTimbrado="([^"]+)"/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
interface PerTenantResult {
|
||||
tenantId: string;
|
||||
rfc: string;
|
||||
databaseName: string;
|
||||
scanned: number;
|
||||
updatedFechaEmision: number;
|
||||
updatedFechaCert: number;
|
||||
noChange: number;
|
||||
noXmlMatch: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function backfillTenant(tenantId: string, rfc: string, databaseName: string): Promise<PerTenantResult> {
|
||||
const result: PerTenantResult = {
|
||||
tenantId, rfc, databaseName,
|
||||
scanned: 0, updatedFechaEmision: 0, updatedFechaCert: 0, noChange: 0, noXmlMatch: 0,
|
||||
};
|
||||
const pool = await tenantDb.getPool(tenantId, databaseName);
|
||||
|
||||
const { rows } = await pool.query<{
|
||||
id: number;
|
||||
uuid: string;
|
||||
fecha_emision: Date;
|
||||
fecha_cert_sat: Date | null;
|
||||
xml_original: string;
|
||||
}>(
|
||||
`SELECT id, uuid, fecha_emision, fecha_cert_sat, xml_original
|
||||
FROM cfdis
|
||||
WHERE xml_original IS NOT NULL
|
||||
ORDER BY id`,
|
||||
);
|
||||
result.scanned = rows.length;
|
||||
if (rows.length === 0) return result;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
for (const row of rows) {
|
||||
const fechaXml = extractFechaFromXml(row.xml_original);
|
||||
const fechaTimbradoXml = extractFechaTimbradoFromXml(row.xml_original);
|
||||
|
||||
if (!fechaXml) {
|
||||
result.noXmlMatch++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nuevaFecha = parseLiteral(fechaXml);
|
||||
const nuevaFechaCert = fechaTimbradoXml ? parseLiteral(fechaTimbradoXml) : null;
|
||||
|
||||
if (!nuevaFecha) {
|
||||
result.noXmlMatch++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fechaEmisionActual = row.fecha_emision?.toISOString();
|
||||
const fechaCertActual = row.fecha_cert_sat?.toISOString();
|
||||
const fechaEmisionNueva = nuevaFecha.toISOString();
|
||||
const fechaCertNueva = nuevaFechaCert?.toISOString();
|
||||
|
||||
let updatedThis = false;
|
||||
if (fechaEmisionActual !== fechaEmisionNueva) {
|
||||
await client.query(
|
||||
`UPDATE cfdis SET fecha_emision = $2 WHERE id = $1`,
|
||||
[row.id, nuevaFecha],
|
||||
);
|
||||
result.updatedFechaEmision++;
|
||||
updatedThis = true;
|
||||
}
|
||||
if (nuevaFechaCert && fechaCertActual !== fechaCertNueva) {
|
||||
await client.query(
|
||||
`UPDATE cfdis SET fecha_cert_sat = $2 WHERE id = $1`,
|
||||
[row.id, nuevaFechaCert],
|
||||
);
|
||||
result.updatedFechaCert++;
|
||||
updatedThis = true;
|
||||
}
|
||||
if (!updatedThis) result.noChange++;
|
||||
}
|
||||
|
||||
if (DRY_RUN) await client.query('ROLLBACK');
|
||||
else await client.query('COMMIT');
|
||||
} catch (err: any) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
result.error = err?.message || String(err);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`=== Backfill fechas (fecha_emision + fecha_cert_sat) ${DRY_RUN ? '(DRY RUN)' : ''} ===\n`);
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
where: { active: true },
|
||||
select: { id: true, rfc: true, databaseName: true },
|
||||
orderBy: { rfc: 'asc' },
|
||||
});
|
||||
console.log(`Tenants activos: ${tenants.length}\n`);
|
||||
|
||||
const results: PerTenantResult[] = [];
|
||||
for (const t of tenants) {
|
||||
process.stdout.write(`[${t.rfc}] ... `);
|
||||
try {
|
||||
const r = await backfillTenant(t.id, t.rfc, t.databaseName);
|
||||
results.push(r);
|
||||
if (r.error) console.log(`ERROR: ${r.error}`);
|
||||
else if (r.scanned === 0) console.log(`sin XMLs (skip)`);
|
||||
else console.log(
|
||||
`scan=${r.scanned} upd_emision=${r.updatedFechaEmision} upd_cert=${r.updatedFechaCert} ` +
|
||||
`sin_cambio=${r.noChange} sin_match=${r.noXmlMatch}${DRY_RUN ? ' (rolled back)' : ''}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.log(`FATAL: ${err?.message || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
const totalScan = results.reduce((s, r) => s + r.scanned, 0);
|
||||
const totalUpdEm = results.reduce((s, r) => s + r.updatedFechaEmision, 0);
|
||||
const totalUpdCert = results.reduce((s, r) => s + r.updatedFechaCert, 0);
|
||||
const tFail = results.filter(r => r.error).length;
|
||||
|
||||
console.log(`\n=== Resumen ===`);
|
||||
console.log(` Tenants procesados: ${results.length}`);
|
||||
console.log(` CFDIs escaneados: ${totalScan}`);
|
||||
console.log(` fecha_emision actualizada: ${totalUpdEm}`);
|
||||
console.log(` fecha_cert_sat actualizada: ${totalUpdCert}`);
|
||||
if (tFail > 0) console.log(` Tenants con error: ${tFail}`);
|
||||
|
||||
await prisma.$disconnect();
|
||||
process.exit(tFail > 0 ? 1 : 0);
|
||||
}
|
||||
main().catch(async e => { console.error(e); await prisma.$disconnect().catch(() => {}); process.exit(1); });
|
||||
Reference in New Issue
Block a user