Files
HoruxDespachosNuevo/apps/api/scripts/backfill-regimen-fiscal.ts
Horux Dev 099f34c903 feat(sat): reporte diario de errores SAT por proxy
- Agrega tabla sat_proxy_errors con proxy_usado, error_code, stage, message.
- sat-client.service.ts expone proxyInfo usado en cada conexión SAT.
- sat.service.ts registra errores de bloqueo/devolución del SAT en sat_proxy_errors
  y guarda proxyUsed en sat_sync_jobs.
- Nuevo job sat-proxy-report.job.ts: cron 8 AM CDMX, envía email a ADMIN_EMAIL
  con errores de las últimas 24h agrupados por proxy/error_code.
- Template de email sat-proxy-report.ts y método sendSatProxyReport.
- Registra el cron en src/index.ts.
- Actualiza docs/SAT-SYNC-IMPLEMENTATION.md.
2026-08-03 14:27:19 +00:00

134 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dotenv/config';
import { Client } from 'pg';
// Backfill NO destructivo de contribuyentes.regimen_fiscal desde la CSF ya
// almacenada en constancias_situacion_fiscal (tabla del tenant). Replica la
// lógica de matching de consultarConstanciaContribuyente para que el resultado
// sea idéntico al de una descarga fresca. Solo toca filas con regimen_fiscal
// NULL o '' (COALESCE semantics). APPLY=1 para escribir; default = dry-run.
const APPLY = process.env.APPLY === '1';
const PG = {
host: 'localhost',
port: 5432,
user: 'postgres',
password: process.env.PGPASSWORD,
};
const CONTROL_DB = 'horux360';
// Réplica exacta del `norm` de consultarConstanciaContribuyente:
// NFD + strip de diacríticos (U+0300U+036F) + lowercase + trim.
const DIACRITICS_RE = new RegExp('[̀-ͯ]', 'g');
const norm = (s: string) =>
s.normalize('NFD').replace(DIACRITICS_RE, '').toLowerCase().trim();
interface CatalogoReg { clave: string; descripcion: string; }
function clavesFromCsfRegimenes(
regimenes: Array<{ nombre: string; fechaFin?: string | null }> | undefined | null,
catalogo: CatalogoReg[],
): string[] {
if (!regimenes?.length) return [];
const found: string[] = [];
for (const reg of regimenes) {
if (reg.fechaFin) continue; // solo activos
const regNorm = norm(reg.nombre);
let best: { clave: string; score: number } | null = null;
for (const r of catalogo) {
const catNorm = norm(r.descripcion);
if (regNorm === catNorm || regNorm.includes(catNorm) || catNorm.includes(regNorm)) {
const score = catNorm.length;
if (!best || score > best.score) best = { clave: r.clave, score };
}
}
if (best) found.push(best.clave);
}
return [...new Set(found)];
}
async function main() {
const control = new Client({ ...PG, database: CONTROL_DB });
await control.connect();
const { rows: catalogo } = await control.query<CatalogoReg>(
`SELECT clave, descripcion FROM regimenes WHERE activo = true`,
);
const { rows: tenants } = await control.query<{ id: string; database_name: string; rfc: string | null }>(
`SELECT id, database_name, rfc FROM tenants
WHERE active = true AND database_name IS NOT NULL AND database_name NOT LIKE '%\\_deleted\\_%'
ORDER BY database_name`,
);
let totalContrib = 0;
let totalUpdated = 0;
const report: Array<{ db: string; rfc: string; claves: string; applied: boolean }> = [];
for (const t of tenants) {
const tdb = new Client({ ...PG, database: t.database_name });
try {
await tdb.connect();
} catch (e: any) {
console.log(`SKIP ${t.database_name}: no se pudo conectar (${e.message})`);
continue;
}
try {
const { rows: hasTbl } = await tdb.query(
`SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='constancias_situacion_fiscal' LIMIT 1`,
);
if (hasTbl.length === 0) continue;
const { rows: contribs } = await tdb.query<{ entidad_id: string; rfc: string }>(
`SELECT entidad_id, rfc FROM contribuyentes
WHERE regimen_fiscal IS NULL OR regimen_fiscal = ''`,
);
if (contribs.length === 0) continue;
for (const c of contribs) {
totalContrib++;
const { rows: csfRows } = await tdb.query<{ regimenes: any }>(
`SELECT datos->'regimenes' AS regimenes FROM constancias_situacion_fiscal
WHERE UPPER(rfc) = UPPER($1) ORDER BY fecha_consulta DESC LIMIT 1`,
[c.rfc],
);
const regimenes = csfRows[0]?.regimenes;
const claves = clavesFromCsfRegimenes(regimenes, catalogo);
if (claves.length === 0) {
report.push({ db: t.database_name, rfc: c.rfc, claves: '(sin match en CSF)', applied: false });
continue;
}
const csv = claves.join(',');
let applied = false;
if (APPLY) {
const { rowCount } = await tdb.query(
`UPDATE contribuyentes SET regimen_fiscal = $1
WHERE entidad_id = $2 AND (regimen_fiscal IS NULL OR regimen_fiscal = '')`,
[csv, c.entidad_id],
);
applied = (rowCount ?? 0) > 0;
if (applied) totalUpdated++;
}
report.push({ db: t.database_name, rfc: c.rfc, claves: csv, applied });
}
} catch (e: any) {
console.log(`ERR ${t.database_name}: ${e.message}`);
} finally {
await tdb.end().catch(() => {});
}
}
await control.end();
console.log(`\n=== ${APPLY ? 'APPLY' : 'DRY-RUN'} regimen_fiscal backfill ===`);
console.log(`catálogo regimenes activos: ${catalogo.length}`);
console.log(`contribuyentes con regimen_fiscal vacío: ${totalContrib}`);
for (const r of report) {
const tag = APPLY ? (r.applied ? 'UPDATED' : 'skip') : 'would-update';
console.log(` [${tag}] ${r.db} ${r.rfc} -> ${r.claves}`);
}
if (APPLY) console.log(`\nTOTAL ACTUALIZADOS: ${totalUpdated}`);
else console.log(`\n(dry-run: ninguna escritura. Corre con APPLY=1 para aplicar.)`);
}
main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); });