Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
import type { Pool } from 'pg';
export interface CreateContribuyenteData {
rfc: string;
razonSocial: string;
regimenFiscal?: string;
codigoPostal?: string;
domicilio?: Record<string, unknown>;
supervisorUserId?: string;
}
export interface ContribuyenteRow {
id: string;
tipo: string;
nombre: string;
identificador: string;
supervisorUserId: string | null;
active: boolean;
createdAt: string;
rfc: string;
regimenFiscal: string | null;
codigoPostal: string | null;
domicilio: Record<string, unknown> | null;
}
export async function listContribuyentes(pool: Pool, entidadIds?: string[]): Promise<ContribuyenteRow[]> {
let query = `
SELECT
e.id, e.tipo, e.nombre, e.identificador,
e.supervisor_user_id AS "supervisorUserId",
e.active, e.created_at AS "createdAt",
c.rfc, c.regimen_fiscal AS "regimenFiscal",
c.codigo_postal AS "codigoPostal", c.domicilio
FROM entidades_gestionadas e
JOIN contribuyentes c ON c.entidad_id = e.id
WHERE e.active = true
`;
const params: unknown[] = [];
if (entidadIds !== undefined) {
if (entidadIds.length === 0) return []; // No access = empty list
query += ` AND e.id = ANY($1)`;
params.push(entidadIds);
}
query += ' ORDER BY e.created_at DESC';
const { rows } = await pool.query(query, params);
return rows;
}
export async function getContribuyenteById(pool: Pool, id: string): Promise<ContribuyenteRow | null> {
const { rows } = await pool.query(`
SELECT
e.id, e.tipo, e.nombre, e.identificador,
e.supervisor_user_id AS "supervisorUserId",
e.active, e.created_at AS "createdAt",
c.rfc, c.regimen_fiscal AS "regimenFiscal",
c.codigo_postal AS "codigoPostal", c.domicilio
FROM entidades_gestionadas e
JOIN contribuyentes c ON c.entidad_id = e.id
WHERE e.id = $1
`, [id]);
return rows[0] ?? null;
}
export async function createContribuyente(pool: Pool, data: CreateContribuyenteData): Promise<ContribuyenteRow> {
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: [entidad] } = await client.query(`
INSERT INTO entidades_gestionadas (tipo, nombre, identificador, supervisor_user_id)
VALUES ('CONTRIBUYENTE', $1, $2, $3)
RETURNING id
`, [data.razonSocial, data.rfc.toUpperCase(), data.supervisorUserId ?? null]);
await client.query(`
INSERT INTO contribuyentes (entidad_id, rfc, regimen_fiscal, codigo_postal, domicilio)
VALUES ($1, $2, $3, $4, $5)
`, [entidad.id, data.rfc.toUpperCase(), data.regimenFiscal ?? null, data.codigoPostal ?? null, data.domicilio ? JSON.stringify(data.domicilio) : null]);
await client.query('COMMIT');
// Backfill: claim existing CFDIs that match this RFC
await backfillCfdiContribuyente(pool, entidad.id, data.rfc.toUpperCase()).catch(
(err) => console.error('[Contribuyente] Backfill CFDIs failed (non-blocking):', err)
);
return (await getContribuyenteById(pool, entidad.id))!;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
export async function updateContribuyente(pool: Pool, id: string, data: Partial<CreateContribuyenteData>): Promise<ContribuyenteRow | null> {
const existing = await getContribuyenteById(pool, id);
if (!existing) return null;
const client = await pool.connect();
try {
await client.query('BEGIN');
// Update entidades_gestionadas if needed
const entidadSets: string[] = [];
const entidadVals: unknown[] = [];
let idx = 1;
if (data.razonSocial) {
entidadSets.push(`nombre = $${idx}`, `identificador = $${idx}`);
entidadVals.push(data.razonSocial);
idx++;
}
if (data.supervisorUserId !== undefined) {
entidadSets.push(`supervisor_user_id = $${idx}`);
entidadVals.push(data.supervisorUserId);
idx++;
}
if (entidadSets.length > 0) {
entidadSets.push('updated_at = now()');
entidadVals.push(id);
await client.query(`UPDATE entidades_gestionadas SET ${entidadSets.join(', ')} WHERE id = $${idx}`, entidadVals);
}
// Update contribuyentes if needed
const contribSets: string[] = [];
const contribVals: unknown[] = [];
idx = 1;
if (data.regimenFiscal !== undefined) { contribSets.push(`regimen_fiscal = $${idx}`); contribVals.push(data.regimenFiscal); idx++; }
if (data.codigoPostal !== undefined) { contribSets.push(`codigo_postal = $${idx}`); contribVals.push(data.codigoPostal); idx++; }
if (data.domicilio !== undefined) { contribSets.push(`domicilio = $${idx}`); contribVals.push(JSON.stringify(data.domicilio)); idx++; }
if (contribSets.length > 0) {
contribVals.push(id);
await client.query(`UPDATE contribuyentes SET ${contribSets.join(', ')} WHERE entidad_id = $${idx}`, contribVals);
}
await client.query('COMMIT');
return (await getContribuyenteById(pool, id))!;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
export async function deactivateContribuyente(pool: Pool, id: string): Promise<boolean> {
const { rowCount } = await pool.query(
'UPDATE entidades_gestionadas SET active = false, updated_at = now() WHERE id = $1',
[id]
);
return (rowCount ?? 0) > 0;
}
/**
* Assigns contribuyente_id to CFDIs that match the RFC (emisor or receptor).
* Runs after contribuyente creation and can be called manually for backfill.
* Only updates CFDIs where contribuyente_id IS NULL (doesn't override).
*/
export async function backfillCfdiContribuyente(pool: Pool, contribuyenteId: string, rfc: string): Promise<number> {
const { rowCount } = await pool.query(`
UPDATE cfdis
SET contribuyente_id = $1
WHERE contribuyente_id IS NULL
AND (rfc_emisor = $2 OR rfc_receptor = $2)
`, [contribuyenteId, rfc]);
const count = rowCount ?? 0;
if (count > 0) {
console.log(`[Backfill] Assigned ${count} CFDIs to contribuyente ${rfc} (${contribuyenteId})`);
}
return count;
}
/**
* Backfills ALL contribuyentes in the tenant BD. Useful after initial SAT sync.
*/
export async function backfillAllContribuyentes(pool: Pool): Promise<number> {
const { rows } = await pool.query('SELECT entidad_id, rfc FROM contribuyentes');
let total = 0;
for (const { entidad_id, rfc } of rows) {
total += await backfillCfdiContribuyente(pool, entidad_id, rfc);
}
return total;
}