164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|