Update: nueva version Horux Despachos

This commit is contained in:
consultoria-as
2026-04-27 22:09:36 -06:00
commit 6b36db1403
614 changed files with 125926 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
/**
* Backfill de `saldo_pendiente_mxn` para CFDIs I PPD vigentes. Computa el
* saldo con la fórmula centralizada en `utils/saldo.ts` (pagos P + NC no-07
* + anticipo aplicado si es I/07) y lo persiste.
*
* Idempotente: corrido varias veces produce el mismo resultado. Safe para
* repetir después de un sync SAT masivo o si se sospecha drift.
*
* Uso:
* pnpm --filter @horux/api exec tsx scripts/backfill-saldo-pendiente.ts # ejecuta
* pnpm --filter @horux/api exec tsx scripts/backfill-saldo-pendiente.ts --dry # reporta sin escribir
*/
import { prisma, tenantDb } from '../src/config/database.js';
import { saldoComputadoExpr } from '../src/utils/saldo.js';
const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run');
interface PerTenantResult {
tenantId: string;
rfc: string;
databaseName: string;
iPpdsVigentes: number;
actualizadas: number;
saldoTotalAntes: number;
saldoTotalDespues: number;
error?: string;
}
async function backfillTenant(
tenantId: string,
rfc: string,
databaseName: string,
): Promise<PerTenantResult> {
const result: PerTenantResult = {
tenantId,
rfc,
databaseName,
iPpdsVigentes: 0,
actualizadas: 0,
saldoTotalAntes: 0,
saldoTotalDespues: 0,
};
const pool = await tenantDb.getPool(tenantId, databaseName);
const { rows: count } = await pool.query<{ n: number; suma: string }>(
`SELECT COUNT(*)::int AS n, COALESCE(SUM(COALESCE(saldo_pendiente_mxn, total_mxn)), 0) AS suma
FROM cfdis
WHERE tipo_comprobante = 'I' AND metodo_pago = 'PPD'
AND status NOT IN ('Cancelado', '0')`,
);
result.iPpdsVigentes = count[0]?.n || 0;
result.saldoTotalAntes = Number(count[0]?.suma || 0);
if (result.iPpdsVigentes === 0) return result;
const client = await pool.connect();
try {
await client.query('BEGIN');
// UPDATE masivo con la fórmula centralizada (misma que hooks y reporte).
const expr = saldoComputadoExpr('c');
const { rowCount } = await client.query(
`UPDATE cfdis c
SET saldo_pendiente_mxn = ${expr}
WHERE c.tipo_comprobante = 'I'
AND c.metodo_pago = 'PPD'
AND c.status NOT IN ('Cancelado', '0')`,
);
result.actualizadas = rowCount ?? 0;
const { rows: cntDespues } = await client.query<{ suma: string }>(
`SELECT COALESCE(SUM(COALESCE(saldo_pendiente_mxn, total_mxn)), 0) AS suma
FROM cfdis
WHERE tipo_comprobante = 'I' AND metodo_pago = 'PPD'
AND status NOT IN ('Cancelado', '0')`,
);
result.saldoTotalDespues = Number(cntDespues[0]?.suma || 0);
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;
}
function fmt(n: number): string {
return n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
async function main() {
console.log(`=== Backfill saldo_pendiente_mxn ${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}] ... `);
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.iPpdsVigentes === 0) {
console.log(`sin I PPD vigentes (skip)`);
} else {
const delta = r.saldoTotalDespues - r.saldoTotalAntes;
console.log(
`I_PPD=${r.iPpdsVigentes} upd=${r.actualizadas} ` +
`antes=${fmt(r.saldoTotalAntes)} despues=${fmt(r.saldoTotalDespues)} ` +
`Δ=${delta >= 0 ? '+' : ''}${fmt(delta)}${DRY_RUN ? ' (rolled back)' : ''}`,
);
}
} catch (err: any) {
console.log(`FATAL: ${err?.message || err}`);
results.push({
tenantId: t.id,
rfc: t.rfc,
databaseName: t.databaseName,
iPpdsVigentes: 0,
actualizadas: 0,
saldoTotalAntes: 0,
saldoTotalDespues: 0,
error: err?.message || String(err),
});
}
}
const totalI = results.reduce((s, r) => s + r.iPpdsVigentes, 0);
const totalAntes = results.reduce((s, r) => s + r.saldoTotalAntes, 0);
const totalDespues = results.reduce((s, r) => s + r.saldoTotalDespues, 0);
const tenantsFailed = results.filter(r => r.error).length;
console.log(`\n=== Resumen ===`);
console.log(` Tenants procesados: ${results.length}`);
console.log(` I PPD vigentes total: ${totalI}`);
console.log(` Saldo total antes: ${fmt(totalAntes)}`);
console.log(` Saldo total después: ${fmt(totalDespues)}${DRY_RUN ? ' (rolled back)' : ''}`);
console.log(` Delta (recuperado): ${fmt(totalAntes - totalDespues)} (saldo que ya no está pendiente)`);
if (tenantsFailed > 0) console.log(` Tenants con error: ${tenantsFailed}`);
await prisma.$disconnect();
process.exit(tenantsFailed > 0 ? 1 : 0);
}
main().catch(async (err) => {
console.error('Fatal:', err);
await prisma.$disconnect().catch(() => {});
process.exit(1);
});