Update: nueva version Horux Despachos
This commit is contained in:
163
apps/api/scripts/backfill-saldo-pendiente.ts
Normal file
163
apps/api/scripts/backfill-saldo-pendiente.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user