Initial commit - Horux Despachos NL
This commit is contained in:
209
apps/api/scripts/backfill-cfdis-relaciones.ts
Normal file
209
apps/api/scripts/backfill-cfdis-relaciones.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Backfill de cfdis.cfdi_tipo_relacion + cfdis.cfdis_relacionados desde
|
||||
* xml_original para CFDIs pre-migración 032.
|
||||
*
|
||||
* Criterio: WHERE xml_original IS NOT NULL AND cfdi_tipo_relacion IS NULL.
|
||||
* Re-usa `parseXml()` para mantener la lógica de extracción idéntica al sync.
|
||||
* Solo escribe si el parser extrae `cfdiTipoRelacion` no-nulo — los CFDIs sin
|
||||
* CfdiRelacionados se siguen dejando con NULL (distinguible de "no procesado"
|
||||
* via el filtro `cfdi_tipo_relacion IS NULL` porque el WHERE al final del run
|
||||
* ya no los va a volver a tocar — pero cada invocación empieza desde el mismo
|
||||
* filtro, por eso es idempotente: los sin-relación se re-parsean cada vez pero
|
||||
* no se escribe nada).
|
||||
*
|
||||
* Uso:
|
||||
* pnpm --filter @horux/api exec tsx scripts/backfill-cfdis-relaciones.ts # ejecuta
|
||||
* pnpm --filter @horux/api exec tsx scripts/backfill-cfdis-relaciones.ts --dry # reporta sin escribir
|
||||
*/
|
||||
import { prisma, tenantDb } from '../src/config/database.js';
|
||||
import { parseXml } from '../src/services/sat/sat-parser.service.js';
|
||||
|
||||
const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run');
|
||||
|
||||
interface PerTenantResult {
|
||||
tenantId: string;
|
||||
rfc: string;
|
||||
databaseName: string;
|
||||
scanned: number;
|
||||
parsedOk: number;
|
||||
parseFailed: number;
|
||||
withRelation: number;
|
||||
updated: number;
|
||||
byTipoRelacion: Record<string, number>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function backfillTenant(
|
||||
tenantId: string,
|
||||
rfc: string,
|
||||
databaseName: string,
|
||||
): Promise<PerTenantResult> {
|
||||
const result: PerTenantResult = {
|
||||
tenantId,
|
||||
rfc,
|
||||
databaseName,
|
||||
scanned: 0,
|
||||
parsedOk: 0,
|
||||
parseFailed: 0,
|
||||
withRelation: 0,
|
||||
updated: 0,
|
||||
byTipoRelacion: {},
|
||||
};
|
||||
|
||||
const pool = await tenantDb.getPool(tenantId, databaseName);
|
||||
|
||||
const { rows } = await pool.query<{
|
||||
id: number;
|
||||
uuid: string;
|
||||
type: string;
|
||||
xml_original: string | null;
|
||||
}>(
|
||||
`SELECT id, uuid, type, xml_original
|
||||
FROM cfdis
|
||||
WHERE xml_original IS NOT NULL
|
||||
AND cfdi_tipo_relacion IS 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) {
|
||||
if (!row.xml_original) continue;
|
||||
|
||||
const downloadType = row.type === 'EMITIDO' ? 'emitidos' : 'recibidos';
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseXml(row.xml_original, downloadType);
|
||||
} catch {
|
||||
result.parseFailed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parsed) {
|
||||
result.parseFailed++;
|
||||
continue;
|
||||
}
|
||||
result.parsedOk++;
|
||||
|
||||
if (!parsed.cfdiTipoRelacion) continue;
|
||||
|
||||
result.withRelation++;
|
||||
const tr = parsed.cfdiTipoRelacion;
|
||||
result.byTipoRelacion[tr] = (result.byTipoRelacion[tr] || 0) + 1;
|
||||
|
||||
await client.query(
|
||||
`UPDATE cfdis
|
||||
SET cfdi_tipo_relacion = $2,
|
||||
cfdis_relacionados = $3,
|
||||
actualizado_en = NOW()
|
||||
WHERE id = $1`,
|
||||
[row.id, parsed.cfdiTipoRelacion, parsed.cfdisRelacionados],
|
||||
);
|
||||
result.updated++;
|
||||
}
|
||||
|
||||
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 cfdis CfdiRelacionados ${DRY_RUN ? '(DRY RUN — no writes)' : ''} ===\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}] (${t.databaseName}) ... `);
|
||||
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 CFDIs candidatos (skip)`);
|
||||
} else {
|
||||
const tiposStr = Object.entries(r.byTipoRelacion)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([tr, n]) => `${tr}:${n}`)
|
||||
.join(', ');
|
||||
console.log(
|
||||
`scan=${r.scanned} parsed=${r.parsedOk} fail=${r.parseFailed} rel=${r.withRelation} upd=${r.updated}${
|
||||
tiposStr ? ` [${tiposStr}]` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`FATAL: ${err?.message || err}`);
|
||||
results.push({
|
||||
tenantId: t.id,
|
||||
rfc: t.rfc,
|
||||
databaseName: t.databaseName,
|
||||
scanned: 0,
|
||||
parsedOk: 0,
|
||||
parseFailed: 0,
|
||||
withRelation: 0,
|
||||
updated: 0,
|
||||
byTipoRelacion: {},
|
||||
error: err?.message || String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const totalScanned = results.reduce((s, r) => s + r.scanned, 0);
|
||||
const totalUpdated = results.reduce((s, r) => s + r.updated, 0);
|
||||
const totalParseFailed = results.reduce((s, r) => s + r.parseFailed, 0);
|
||||
const tenantsTouched = results.filter(r => r.updated > 0).length;
|
||||
const tenantsFailed = results.filter(r => r.error).length;
|
||||
|
||||
const tiposGlobales: Record<string, number> = {};
|
||||
for (const r of results) {
|
||||
for (const [tr, n] of Object.entries(r.byTipoRelacion)) {
|
||||
tiposGlobales[tr] = (tiposGlobales[tr] || 0) + n;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== Resumen ===`);
|
||||
console.log(` Tenants procesados: ${results.length}`);
|
||||
console.log(` Tenants con backfill: ${tenantsTouched}`);
|
||||
console.log(` CFDIs escaneados: ${totalScanned}`);
|
||||
console.log(` CFDIs actualizados: ${totalUpdated}${DRY_RUN ? ' (rolled back)' : ''}`);
|
||||
if (totalParseFailed > 0) console.log(` CFDIs parse falló: ${totalParseFailed}`);
|
||||
if (tenantsFailed > 0) console.log(` Tenants con error: ${tenantsFailed}`);
|
||||
if (Object.keys(tiposGlobales).length > 0) {
|
||||
console.log(` Desglose TipoRelacion:`);
|
||||
for (const [tr, n] of Object.entries(tiposGlobales).sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${tr}: ${n}`);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
process.exit(tenantsFailed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
console.error('Fatal:', err);
|
||||
await prisma.$disconnect().catch(() => {});
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user