fix(impuestos): desactivar JIT en queries con subplans correlacionados

- Agrega helper withJitOff en impuestos.service.ts
- Ejecuta getResumenIva, getIvaMensual y readResumenIvaFromCache con SET LOCAL jit = off
- Evita compilación JIT de ~17s en queries con costo estimado alto

feat(contribuyentes): auto-asignar a cartera del supervisor

- Al crear contribuyente con supervisorUserId, se agrega automáticamente
  a todas las carteras top-level del supervisor

feat(permisos): restricciones de UI por rol en contribuyentes

- Oculta botón Add-ons para roles distintos de owner/cfo
- Oculta botón Eliminar contribuyente para no-owner
- Oculta botón Agregar RFC para auxiliar/visor/cliente/contador

feat(cfdi): ver CFDI desde conceptos y forma de pago en Excel

- Agrega botón Ver CFDI en cada fila de la tabla de Conceptos
- Agrega columna Forma de Pago en export Excel de CFDIs
- Agrega columna Forma de Pago en export individual de CFDI

chore(migraciones): índices GIN para relaciones de activos

- 048: índices btree parciales para activos
- 049: índices GIN para cfdis_relacionados y uuid_relacionado
This commit is contained in:
Horux Dev
2026-05-28 02:38:30 +00:00
parent 138e223361
commit 2208cee87f
14 changed files with 390 additions and 152 deletions

View File

@@ -1,4 +1,4 @@
import type { Pool } from 'pg';
import type { Pool, PoolClient } from 'pg';
import type { IvaMensual, IsrMensual, ResumenIva, IvaRegimenDetalle, ResumenIsr } from '@horux/shared';
import { getRegimenesIgnoradosClaves } from './regimen.service.js';
import {
@@ -106,32 +106,40 @@ const SUM_E_REFERENCING_TRAS = (
esLadoE: string,
considerarActivos: boolean,
considerarNCs: boolean,
) => `COALESCE((
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
) => {
if (!considerarNCs) return '0';
return `COALESCE((
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND e.cfdis_relacionados IS NOT NULL
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
};
const SUM_E_REFERENCING_RET = (
esLadoE: string,
considerarActivos: boolean,
considerarNCs: boolean,
) => `COALESCE((
SELECT SUM(${IVA_RET_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
) => {
if (!considerarNCs) return '0';
return `COALESCE((
SELECT SUM(${IVA_RET_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND e.cfdis_relacionados IS NOT NULL
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
};
// Régimen del contribuyente según su lado: emisor/receptor del CFDI.
// Usa el RFC del contribuyente (via `ctx.esEmisor`/`ctx.esReceptor`) para
// determinar el lado, no el `type` de BD.
@@ -152,16 +160,20 @@ const HAS_E_REFERENCING_MISMO_MES = (
esLadoE: string,
considerarActivos: boolean,
considerarNCs: boolean,
) => `EXISTS (
SELECT 1 FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
AND date_trunc('month', e.fecha_emision)
= date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
)`;
) => {
if (!considerarNCs) return 'FALSE';
return `EXISTS (
SELECT 1 FROM cfdis e
WHERE e.tipo_comprobante = 'E'
AND e.metodo_pago = 'PUE'
AND e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND e.cfdis_relacionados IS NOT NULL
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
AND date_trunc('month', e.fecha_emision)
= date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
)`;
};
// Atribución por lado usando RFC en lugar de `type`. Los buckets son
// factories que reciben el context del contribuyente:
@@ -397,8 +409,8 @@ export async function getIvaMensual(
const añoEnd = `${año}-12-31`;
const extra = buildExtraFilters(considerarActivos, considerarNCs);
const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
pool.query<{ mes: number; trasladado: string; retencion: string }>(`
const { rows: causadoRows } = await withJitOff(pool, (client) =>
client.query<{ mes: number; trasladado: string; retencion: string }>(`
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
@@ -407,8 +419,10 @@ export async function getIvaMensual(
AND ${VIGENTE} AND ${FR}${extra}
AND (${REGIMEN_TENANT}) = ANY($3)
GROUP BY mes
`, [añoStart, añoEnd, TODOS_REGIMENES]),
pool.query<{ mes: number; trasladado: string; retencion: string }>(`
`, [añoStart, añoEnd, TODOS_REGIMENES])
);
const { rows: acreditableRows } = await withJitOff(pool, (client) =>
client.query<{ mes: number; trasladado: string; retencion: string }>(`
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
@@ -417,8 +431,8 @@ export async function getIvaMensual(
AND ${VIGENTE} AND ${FR}${extra}
AND (${REGIMEN_TENANT}) = ANY($3)
GROUP BY mes
`, [añoStart, añoEnd, TODOS_REGIMENES]),
]);
`, [añoStart, añoEnd, TODOS_REGIMENES])
);
perMes = new Map();
for (const row of causadoRows) {
@@ -648,20 +662,22 @@ async function readResumenIvaFromCache(
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
const REGIMEN_TENANT = regimenTenantExpr(ctx);
const acumRow = (await pool.query(`
SELECT
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
(
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
) as total
FROM cfdis
WHERE ${VIGENTE}
AND (${REGIMEN_TENANT}) = ANY($3)
AND ${acumFR}
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])).rows[0];
const acumRow = (await withJitOff(pool, (client) =>
client.query(`
SELECT
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
(
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
) as total
FROM cfdis
WHERE ${VIGENTE}
AND (${REGIMEN_TENANT}) = ANY($3)
AND ${acumFR}
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])
)).rows[0];
// Cache hit retorna 0/empty para los surface IVA No Acreditable. El cache
// aún no persiste esos campos — si se hace crítico para BI, agregar columna
@@ -698,6 +714,29 @@ async function readResumenIvaFromCache(
*
* Algebraicamente: T A R == dashboard.balance, céntimo por céntimo.
*/
/**
* Ejecuta un callback con un client de pool con JIT desactivado (SET LOCAL jit = off).
* Usa una transacción implícita para que el SET LOCAL se restaure automáticamente
* al liberar la conexión. Esto evita que PostgreSQL compile JIT para queries con
* muchos subplans (correlacionados), lo cual puede tardar >15s en queries con
* costo estimado muy alto aunque la ejecución real sea rápida.
*/
async function withJitOff<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('SET LOCAL jit = off');
const result = await fn(client);
await client.query('COMMIT');
return result;
} catch (e) {
await client.query('ROLLBACK').catch(() => {});
throw e;
} finally {
client.release();
}
}
export async function getResumenIva(
pool: Pool,
fechaInicio: string,
@@ -725,10 +764,10 @@ export async function getResumenIva(
if (cached) return cached;
}
// Una query por lado (causado / acreditable). Filtro por RFC via
// ctx.esEmisor/esReceptor (embedded en buckets/signed exprs).
const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
// Queries con JIT off: evitan compilación JIT de >15s en queries con muchos
// subplans correlacionados (activado por costo estimado >100k).
const { rows: causadoRows } = await withJitOff(pool, (client) =>
client.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
SELECT ${REGIMEN_TENANT} as regimen,
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
@@ -737,8 +776,10 @@ export async function getResumenIva(
AND ${VIGENTE} AND ${FR}${extra}
AND (${REGIMEN_TENANT}) = ANY($3)
GROUP BY ${REGIMEN_TENANT}
`, [fechaInicio, fechaFin, TODOS_REGIMENES]),
pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
`, [fechaInicio, fechaFin, TODOS_REGIMENES])
);
const { rows: acreditableRows } = await withJitOff(pool, (client) =>
client.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
SELECT ${REGIMEN_TENANT} as regimen,
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
@@ -747,8 +788,8 @@ export async function getResumenIva(
AND ${VIGENTE} AND ${FR}${extra}
AND (${REGIMEN_TENANT}) = ANY($3)
GROUP BY ${REGIMEN_TENANT}
`, [fechaInicio, fechaFin, TODOS_REGIMENES]),
]);
`, [fechaInicio, fechaFin, TODOS_REGIMENES])
);
// Combinar por régimen: el set de régimenes posibles es la unión de ambos lados.
type Acc = { trasCausado: number; retCausado: number; trasAcreditable: number; retAcreditable: number };
@@ -799,20 +840,22 @@ export async function getResumenIva(
// Acumulado anual (misma fórmula T A R, pero rango = enero → fechaFin).
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
const { rows: [acumRow] } = await pool.query(`
SELECT
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
(
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
) as total
FROM cfdis
WHERE ${VIGENTE}
AND (${REGIMEN_TENANT}) = ANY($3)
AND ${acumFR}${extra}
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES]);
const { rows: [acumRow] } = await withJitOff(pool, (client) =>
client.query(`
SELECT
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
(
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
) as total
FROM cfdis
WHERE ${VIGENTE}
AND (${REGIMEN_TENANT}) = ANY($3)
AND ${acumFR}${extra}
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])
);
// IVA No Acreditable surface (Art. 5 LIVA fracción I + Art. 27 fracción III LISR).
// No participa en `resultado` — ya excluido del `acreditable` arriba via filtro