diff --git a/apps/api/src/controllers/alertas.controller.ts b/apps/api/src/controllers/alertas.controller.ts index 6e058ab..7b75db3 100644 --- a/apps/api/src/controllers/alertas.controller.ts +++ b/apps/api/src/controllers/alertas.controller.ts @@ -333,7 +333,7 @@ export async function getCancelados(req: Request, res: Response, next: NextFunct total_mxn as "totalMxn", fecha_cancelacion as "fechaCancelacion" FROM cfdis WHERE status IN ('Cancelado', '0') - AND fecha_emision >= $1::date + AND (fecha_emision - interval '1 hour') >= $1::date ${cf} ORDER BY fecha_emision DESC `, [hace5.toISOString().split('T')[0]]); @@ -364,7 +364,7 @@ export async function getCancelacionesPeriodoAnterior(req: Request, res: Respons FROM cfdis WHERE status IN ('Cancelado', '0') AND fecha_cancelacion >= $1::date - AND fecha_emision < $1::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < $1::date ${cf} ORDER BY fecha_cancelacion DESC `, [inicioMes]); diff --git a/apps/api/src/controllers/contribuyente-config.controller.ts b/apps/api/src/controllers/contribuyente-config.controller.ts index 8d49172..1879f0f 100644 --- a/apps/api/src/controllers/contribuyente-config.controller.ts +++ b/apps/api/src/controllers/contribuyente-config.controller.ts @@ -13,7 +13,7 @@ export async function uploadFiel(req: Request, res: Response, next: NextFunction return next(new AppError(400, 'cerFile, keyFile y password son requeridos')); } const contribuyenteId = String(req.params.id); - const contrib = await getContribuyenteById(req.tenantPool!, contribuyenteId); + const contrib = await getContribuyenteById(req.tenantPool!, contribuyenteId, req.user!.tenantId); if (!contrib) return next(new AppError(404, 'Contribuyente no encontrado')); const result = await fielService.uploadFielContribuyente(req.tenantPool!, contribuyenteId, cerFile, keyFile, password); @@ -62,7 +62,7 @@ export async function deleteFiel(req: Request, res: Response, next: NextFunction export async function createOrg(req: Request, res: Response, next: NextFunction) { try { const contribuyenteId = String(req.params.id); - const contrib = await getContribuyenteById(req.tenantPool!, contribuyenteId); + const contrib = await getContribuyenteById(req.tenantPool!, contribuyenteId, req.user!.tenantId); if (!contrib) return next(new AppError(404, 'Contribuyente no encontrado')); const result = await facturapiService.createOrgContribuyente(req.tenantPool!, contribuyenteId, contrib.nombre); diff --git a/apps/api/src/controllers/contribuyente.controller.ts b/apps/api/src/controllers/contribuyente.controller.ts index a0ca37f..5ba5baf 100644 --- a/apps/api/src/controllers/contribuyente.controller.ts +++ b/apps/api/src/controllers/contribuyente.controller.ts @@ -40,14 +40,14 @@ const updateSchema = createSchema.partial(); export async function list(req: Request, res: Response, next: NextFunction) { try { const visibleIds = await getEntidadesVisibles(req.tenantPool!, req.user!.userId, req.user!.role); - const rows = await contribuyenteService.listContribuyentes(req.tenantPool!, visibleIds); + const rows = await contribuyenteService.listContribuyentes(req.tenantPool!, visibleIds, req.user!.tenantId); return res.json({ data: rows }); } catch (err) { return next(err); } } export async function getById(req: Request, res: Response, next: NextFunction) { try { - const row = await contribuyenteService.getContribuyenteById(req.tenantPool!, String(req.params.id)); + const row = await contribuyenteService.getContribuyenteById(req.tenantPool!, String(req.params.id), req.user!.tenantId); if (!row) return next(new AppError(404, 'Contribuyente no encontrado')); return res.json(row); } catch (err) { return next(err); } diff --git a/apps/api/src/migrations/tenant/001_initial_schema.sql b/apps/api/src/migrations/tenant/001_initial_schema.sql index 76cde40..0aacf61 100644 --- a/apps/api/src/migrations/tenant/001_initial_schema.sql +++ b/apps/api/src/migrations/tenant/001_initial_schema.sql @@ -118,6 +118,10 @@ CREATE TABLE IF NOT EXISTS cfdis ( facturapi_id VARCHAR(50), regimen_fiscal_emisor VARCHAR(3), regimen_fiscal_receptor VARCHAR(3), + periodicidad VARCHAR(2), + meses_global VARCHAR(10), + año_global VARCHAR(4), + fecha_efectiva DATE, creado_en TIMESTAMP DEFAULT NOW(), actualizado_en TIMESTAMP DEFAULT NOW() ); diff --git a/apps/api/src/migrations/tenant/045_factura_global.sql b/apps/api/src/migrations/tenant/045_factura_global.sql new file mode 100644 index 0000000..c61842e --- /dev/null +++ b/apps/api/src/migrations/tenant/045_factura_global.sql @@ -0,0 +1,11 @@ +-- Migration: 007_factura_global +-- Description: Agrega campos de InformacionGlobal y fecha_efectiva para facturas globales + +ALTER TABLE cfdis + ADD COLUMN IF NOT EXISTS periodicidad VARCHAR(2), + ADD COLUMN IF NOT EXISTS meses_global VARCHAR(10), + ADD COLUMN IF NOT EXISTS año_global VARCHAR(4), + ADD COLUMN IF NOT EXISTS fecha_efectiva DATE; + +-- Crear índice para acelerar métricas que filtran por fecha_efectiva +CREATE INDEX IF NOT EXISTS idx_cfdis_fecha_efectiva ON cfdis(fecha_efectiva); diff --git a/apps/api/src/scripts/recalc-metricas.ts b/apps/api/src/scripts/recalc-metricas.ts new file mode 100644 index 0000000..70e0643 --- /dev/null +++ b/apps/api/src/scripts/recalc-metricas.ts @@ -0,0 +1,23 @@ +import { tenantDb } from '../config/database.js'; +import { computeMetricaMensual } from '../services/metricas-compute.service.js'; + +async function main() { + const tenantId = 'c52c2f5d-b1ae-45c6-8cc8-b11c9611618a'; + const dbName = 'horux_hts240708lja'; + const contribuyenteId = '4a1d6014-f705-424b-b185-7740be6a80c6'; + const pool = await tenantDb.getPool(tenantId, dbName); + + for (const mes of [1, 2, 3]) { + console.log(`Recalculando 2026-${String(mes).padStart(2, '0')}...`); + const r = await computeMetricaMensual(pool, tenantId, contribuyenteId, 2026, mes); + console.log(` Filas escritas: ${r.filasEscritas}`); + } + + await pool.end(); + process.exit(0); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/api/src/services/alertas-auto.service.ts b/apps/api/src/services/alertas-auto.service.ts index 7d2ac26..c01bf13 100644 --- a/apps/api/src/services/alertas-auto.service.ts +++ b/apps/api/src/services/alertas-auto.service.ts @@ -176,7 +176,7 @@ async function alertaRiesgoCancelaciones(pool: Pool, contribuyenteId?: string | COUNT(*)::int as total, COUNT(CASE WHEN status IN ('Cancelado', '0') THEN 1 END)::int as cancelados FROM cfdis - WHERE fecha_emision >= $1::date + WHERE (fecha_emision - interval '1 hour') >= $1::date ${cf} `, [fechaDesde]); @@ -359,7 +359,7 @@ async function alertaCancelacionPeriodoAnterior(pool: Pool, contribuyenteId?: st FROM cfdis WHERE status IN ('Cancelado', '0') AND fecha_cancelacion >= $1::date - AND fecha_emision < $1::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < $1::date ${cf} `, [inicioMes]); @@ -529,7 +529,7 @@ async function alertaResicoPfLimiteIngresos( FROM cfdis WHERE type = 'EMITIDO' AND status NOT IN ('Cancelado', '0') - AND EXTRACT(YEAR FROM fecha_emision) = $1 + AND EXTRACT(YEAR FROM COALESCE(fecha_efectiva, fecha_emision - interval '1 hour')) = $1 AND contribuyente_id = $2 `, [año, safeId]); @@ -659,8 +659,8 @@ export async function getDiscrepanciasPorMes( const { rows } = await pool.query(` SELECT - EXTRACT(YEAR FROM fecha_emision)::int as año, - EXTRACT(MONTH FROM fecha_emision)::int as mes, + EXTRACT(YEAR FROM COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'))::int as año, + EXTRACT(MONTH FROM COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'))::int as mes, COUNT(*)::int as count FROM cfdis WHERE type = 'RECIBIDO' AND ${VIGENTE} diff --git a/apps/api/src/services/cfdi.service.ts b/apps/api/src/services/cfdi.service.ts index 41957fc..525ed21 100644 --- a/apps/api/src/services/cfdi.service.ts +++ b/apps/api/src/services/cfdi.service.ts @@ -102,12 +102,12 @@ export async function getCfdis(pool: Pool, filters: CfdiFilters): Promise= $${paramIndex++}::date`; + whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $${paramIndex++}::date`; params.push(filters.fechaInicio); } if (filters.fechaFin) { - whereClause += ` AND fecha_emision <= ($${paramIndex++}::date + interval '1 day')`; + whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') <= ($${paramIndex++}::date + interval '1 day')`; params.push(filters.fechaFin); } @@ -214,11 +214,11 @@ export async function getConceptosList( params.push(filters.estado); } if (filters.fechaInicio) { - whereClause += ` AND c.fecha_emision >= $${paramIndex++}::date`; + whereClause += ` AND COALESCE(c.fecha_efectiva, c.fecha_emision - interval '1 hour') >= $${paramIndex++}::date`; params.push(filters.fechaInicio); } if (filters.fechaFin) { - whereClause += ` AND c.fecha_emision <= ($${paramIndex++}::date + interval '1 day')`; + whereClause += ` AND COALESCE(c.fecha_efectiva, c.fecha_emision - interval '1 hour') <= ($${paramIndex++}::date + interval '1 day')`; params.push(filters.fechaFin); } if (filters.rfc) { @@ -746,7 +746,10 @@ export async function getReceptores(pool: Pool, search: string, limit: number = } export async function getResumenCfdis(pool: Pool, año: number, mes: number, contribuyenteId?: string) { - let whereClause = `WHERE status NOT IN ('Cancelado', '0') AND year = $1 AND month = $2`; + const fi = `${año}-${String(mes).padStart(2, '0')}-01`; + const lastDay = new Date(año, mes, 0).getDate(); + const ff = `${año}-${String(mes).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; + let whereClause = `WHERE status NOT IN ('Cancelado', '0') AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') <= $2::date`; if (contribuyenteId) { const safeId = contribuyenteId.replace(/[^a-f0-9-]/gi, ''); whereClause += ` AND (contribuyente_id = '${safeId}' OR rfc_emisor = (SELECT rfc FROM contribuyentes WHERE entidad_id = '${safeId}') OR rfc_receptor = (SELECT rfc FROM contribuyentes WHERE entidad_id = '${safeId}'))`; @@ -761,7 +764,7 @@ export async function getResumenCfdis(pool: Pool, año: number, mes: number, con COALESCE(SUM(CASE WHEN type = 'RECIBIDO' THEN iva_traslado_mxn ELSE 0 END), 0) as iva_acreditable FROM cfdis ${whereClause} - `, [String(año), String(mes).padStart(2, '0')]); + `, [fi, ff]); const r = rows[0]; return { diff --git a/apps/api/src/services/conciliacion.service.ts b/apps/api/src/services/conciliacion.service.ts index f0c52ed..cb81770 100644 --- a/apps/api/src/services/conciliacion.service.ts +++ b/apps/api/src/services/conciliacion.service.ts @@ -68,11 +68,11 @@ export async function getCfdisConConciliacion( } if (filters.fechaInicio) { - where += ` AND COALESCE(c.fecha_pago_p, c.fecha_emision) >= $${idx++}::date`; + where += ` AND CASE WHEN c.tipo_comprobante = 'P' THEN c.fecha_pago_p - interval '1 hour' ELSE COALESCE(c.fecha_efectiva, c.fecha_emision - interval '1 hour') END >= $${idx++}::date`; params.push(filters.fechaInicio); } if (filters.fechaFin) { - where += ` AND COALESCE(c.fecha_pago_p, c.fecha_emision) <= ($${idx++}::date + interval '1 day')`; + where += ` AND CASE WHEN c.tipo_comprobante = 'P' THEN c.fecha_pago_p - interval '1 hour' ELSE COALESCE(c.fecha_efectiva, c.fecha_emision - interval '1 hour') END <= ($${idx++}::date + interval '1 day')`; params.push(filters.fechaFin); } if (filters.regimen) { diff --git a/apps/api/src/services/contribuyente.service.ts b/apps/api/src/services/contribuyente.service.ts index 251804e..368e0d3 100644 --- a/apps/api/src/services/contribuyente.service.ts +++ b/apps/api/src/services/contribuyente.service.ts @@ -1,4 +1,5 @@ import type { Pool } from 'pg'; +import { prisma } from '../config/database.js'; export interface CreateContribuyenteData { rfc: string; @@ -23,7 +24,61 @@ export interface ContribuyenteRow { domicilio: Record | null; } -export async function listContribuyentes(pool: Pool, entidadIds?: string[]): Promise { +async function fetchTenantFiscalData(tenantId: string) { + const tenant = await prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { + rfc: true, + codigoPostal: true, + calle: true, + numExterior: true, + numInterior: true, + colonia: true, + ciudad: true, + municipio: true, + estado: true, + telefono: true, + }, + }); + if (!tenant) return null; + + const regimenes = await prisma.tenantRegimenActivo.findMany({ + where: { tenantId }, + select: { regimen: { select: { clave: true } } }, + }); + const regimenFiscal = regimenes.map(r => r.regimen.clave).join(',') || null; + + const hasAnyAddress = tenant.calle || tenant.colonia || tenant.ciudad || tenant.municipio || tenant.estado || tenant.codigoPostal; + const domicilio = hasAnyAddress + ? { + calle: tenant.calle || '', + numExterior: tenant.numExterior || '', + numInterior: tenant.numInterior || '', + colonia: tenant.colonia || '', + ciudad: tenant.ciudad || '', + municipio: tenant.municipio || '', + estado: tenant.estado || '', + codigoPostal: tenant.codigoPostal || '', + telefono: tenant.telefono || '', + } + : null; + + return { tenantRfc: tenant.rfc, regimenFiscal, codigoPostal: tenant.codigoPostal, domicilio }; +} + +function mergeContribuyenteWithTenant( + row: ContribuyenteRow, + tenantData: NonNullable>> +): ContribuyenteRow { + return { + ...row, + regimenFiscal: row.regimenFiscal || tenantData.regimenFiscal, + codigoPostal: row.codigoPostal || tenantData.codigoPostal, + domicilio: row.domicilio || tenantData.domicilio, + }; +} + +export async function listContribuyentes(pool: Pool, entidadIds?: string[], tenantId?: string): Promise { let query = ` SELECT e.id, e.tipo, e.nombre, e.identificador, @@ -45,10 +100,20 @@ export async function listContribuyentes(pool: Pool, entidadIds?: string[]): Pro query += ' ORDER BY e.created_at DESC'; const { rows } = await pool.query(query, params); - return rows; + + if (!tenantId) return rows; + + const tenantData = await fetchTenantFiscalData(tenantId); + if (!tenantData) return rows; + + return rows.map((r: ContribuyenteRow) => { + if (r.rfc !== tenantData.tenantRfc) return r; + if (r.regimenFiscal && r.codigoPostal && r.domicilio) return r; + return mergeContribuyenteWithTenant(r, tenantData); + }); } -export async function getContribuyenteById(pool: Pool, id: string): Promise { +export async function getContribuyenteById(pool: Pool, id: string, tenantId?: string): Promise { const { rows } = await pool.query(` SELECT e.id, e.tipo, e.nombre, e.identificador, @@ -60,7 +125,14 @@ export async function getContribuyenteById(pool: Pool, id: string): Promise { diff --git a/apps/api/src/services/dashboard.service.ts b/apps/api/src/services/dashboard.service.ts index 37a3e2b..7b1476f 100644 --- a/apps/api/src/services/dashboard.service.ts +++ b/apps/api/src/services/dashboard.service.ts @@ -109,13 +109,13 @@ export const GRUPO_PM_OTROS = ['601', '603', '607', '608', '610', '611', '614', const TODOS_REGIMENES = [...GRUPO_PF_EMPRESARIAL, ...GRUPO_SUELDOS, ...GRUPO_PM_OTROS]; // Filtro de fecha por rango — normal o conciliación -const FECHA_RANGO = `fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day')`; +const FECHA_RANGO = `COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day')`; // Para CFDIs tipo P (complementos de pago): el ingreso/gasto se reconoce en la // fecha_pago_p (cuándo el cliente realmente pagó), no cuando se emitió el // complemento — el CFDI P puede emitirse hasta el día 5 del mes siguiente al // pago, o incluso después, y cruzar meses (ej. pago de noviembre 2024 con // complemento emitido en mayo 2025). -const FECHA_PAGO_RANGO = `fecha_pago_p >= $1::date AND fecha_pago_p < ($2::date + interval '1 day')`; +const FECHA_PAGO_RANGO = `(fecha_pago_p - interval '1 hour') >= $1::date AND (fecha_pago_p - interval '1 hour') < ($2::date + interval '1 day')`; const FECHA_RANGO_CONCILIACION = `id_conciliacion IS NOT NULL AND id_conciliacion IN ( SELECT id FROM conciliaciones WHERE fecha_de_pago >= $1::date AND fecha_de_pago < ($2::date + interval '1 day') )`; @@ -989,14 +989,14 @@ export async function calcularIvaBalancePorRegimen( AND e.status NOT IN ('Cancelado', '0') AND ${esEmisor.replace(/\brfc_emisor\b/g, 'e.rfc_emisor')} AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|')) - AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision) + AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour')) = date_trunc('month', COALESCE(i.fecha_efectiva, i.fecha_emision - interval '1 hour')) )), 0) as monto FROM cfdis i WHERE ${esEmisor.replace(/\brfc_emisor\b/g, 'i.rfc_emisor')} AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD' AND COALESCE(i.cfdi_tipo_relacion, '') = '07' AND i.status NOT IN ('Cancelado','0') - AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')} + AND ${FR.replace(/\bfecha_efectiva\b/g, 'i.fecha_efectiva').replace(/\bfecha_emision\b/g, 'i.fecha_emision')} AND i.regimen_fiscal_emisor = ANY($3) GROUP BY i.regimen_fiscal_emisor `, [fechaInicio, fechaFin, TODOS_REGIMENES]); @@ -1012,14 +1012,14 @@ export async function calcularIvaBalancePorRegimen( AND e.status NOT IN ('Cancelado', '0') AND ${esReceptor.replace(/\brfc_receptor\b/g, 'e.rfc_receptor')} AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|')) - AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision) + AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour')) = date_trunc('month', COALESCE(i.fecha_efectiva, i.fecha_emision - interval '1 hour')) )), 0) as monto FROM cfdis i WHERE ${esReceptor.replace(/\brfc_receptor\b/g, 'i.rfc_receptor')} AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD' AND COALESCE(i.cfdi_tipo_relacion, '') = '07' AND i.status NOT IN ('Cancelado','0') - AND ${FR.replace(/\bfecha_emision\b/g, 'i.fecha_emision')} + AND ${FR.replace(/\bfecha_efectiva\b/g, 'i.fecha_efectiva').replace(/\bfecha_emision\b/g, 'i.fecha_emision')} AND i.regimen_fiscal_receptor = ANY($3) GROUP BY i.regimen_fiscal_receptor `, [fechaInicio, fechaFin, TODOS_REGIMENES]); diff --git a/apps/api/src/services/export.service.ts b/apps/api/src/services/export.service.ts index 367dfcc..7ec523e 100644 --- a/apps/api/src/services/export.service.ts +++ b/apps/api/src/services/export.service.ts @@ -18,11 +18,11 @@ export async function exportCfdisToExcel( params.push(filters.estado); } if (filters.fechaInicio) { - whereClause += ` AND fecha_emision >= $${paramIndex++}`; + whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $${paramIndex++}`; params.push(filters.fechaInicio); } if (filters.fechaFin) { - whereClause += ` AND fecha_emision <= $${paramIndex++}`; + whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') <= $${paramIndex++}`; params.push(filters.fechaFin); } @@ -74,7 +74,7 @@ export async function exportCfdisToExcel( cfdis.forEach((cfdi: any) => { sheet.addRow({ ...cfdi, - fecha_emision: new Date(cfdi.fecha_emision).toLocaleDateString('es-MX'), + fecha_emision: new Date(new Date(cfdi.fecha_emision).getTime() - 60*60*1000).toLocaleDateString('es-MX'), subtotal: Number(cfdi.subtotal), subtotal_mxn: Number(cfdi.subtotal_mxn), iva_traslado: Number(cfdi.iva_traslado), @@ -103,7 +103,7 @@ export async function exportReporteToExcel( COALESCE(SUM(CASE WHEN type = 'EMITIDO' AND tipo_comprobante = 'I' THEN subtotal_mxn ELSE 0 END), 0) as ingresos, COALESCE(SUM(CASE WHEN type = 'RECIBIDO' AND tipo_comprobante = 'I' THEN subtotal_mxn ELSE 0 END), 0) as egresos FROM cfdis - WHERE status NOT IN ('Cancelado', '0') AND fecha_emision BETWEEN $1 AND $2 + WHERE status NOT IN ('Cancelado', '0') AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') BETWEEN $1 AND $2 `, [fechaInicio, fechaFin]); sheet.columns = [ diff --git a/apps/api/src/services/impuestos.service.ts b/apps/api/src/services/impuestos.service.ts index 446df7e..8e3829b 100644 --- a/apps/api/src/services/impuestos.service.ts +++ b/apps/api/src/services/impuestos.service.ts @@ -22,7 +22,7 @@ const VIGENTE = `status NOT IN ('Cancelado', '0')`; // - otros tipos (I, E, T, N): fecha_emision del comprobante // El CASE se evalúa por fila, garantizando que un P emitido en mayo por un pago // real de noviembre quede contabilizado en noviembre. -const FECHA_EFECTIVA = `CASE WHEN tipo_comprobante = 'P' THEN fecha_pago_p ELSE fecha_emision END`; +const FECHA_EFECTIVA = `CASE WHEN tipo_comprobante = 'P' THEN (fecha_pago_p - interval '1 hour') ELSE COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') END`; const FECHA_RANGO = `${FECHA_EFECTIVA} >= $1::date AND ${FECHA_EFECTIVA} < ($2::date + interval '1 day')`; const FECHA_RANGO_CONCILIACION = `id_conciliacion IS NOT NULL AND id_conciliacion IN ( SELECT id FROM conciliaciones WHERE fecha_de_pago >= $1::date AND fecha_de_pago < ($2::date + interval '1 day') @@ -114,8 +114,8 @@ const SUM_E_REFERENCING_TRAS = ( 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)} + 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, @@ -129,8 +129,8 @@ const SUM_E_REFERENCING_RET = ( 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)} + 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 diff --git a/apps/api/src/services/metricas-compute.service.ts b/apps/api/src/services/metricas-compute.service.ts index 455d82d..1591e57 100644 --- a/apps/api/src/services/metricas-compute.service.ts +++ b/apps/api/src/services/metricas-compute.service.ts @@ -92,8 +92,8 @@ export async function computeMetricaMensual( COUNT(*) FILTER (WHERE status = 'Vigente') AS vigentes, COUNT(*) FILTER (WHERE status IN ('Cancelado','0')) AS cancelados FROM cfdis - WHERE EXTRACT(YEAR FROM (CASE WHEN tipo_comprobante='P' THEN fecha_pago_p ELSE fecha_emision END)) = $1 - AND EXTRACT(MONTH FROM (CASE WHEN tipo_comprobante='P' THEN fecha_pago_p ELSE fecha_emision END)) = $2 + WHERE EXTRACT(YEAR FROM (CASE WHEN tipo_comprobante='P' THEN (fecha_pago_p - interval '1 hour') ELSE COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') END)) = $1 + AND EXTRACT(MONTH FROM (CASE WHEN tipo_comprobante='P' THEN (fecha_pago_p - interval '1 hour') ELSE COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') END)) = $2 AND contribuyente_id = $3 GROUP BY 1, 2 `, [anio, mes, safeContrib]); @@ -227,7 +227,7 @@ export async function backfillTenant( for (const c of contribs) { const { rows: [rango] } = await pool.query<{ min_anio: number | null }>( - `SELECT EXTRACT(YEAR FROM MIN(fecha_emision))::int AS min_anio + `SELECT EXTRACT(YEAR FROM MIN(fecha_emision - interval '1 hour'))::int AS min_anio FROM cfdis WHERE contribuyente_id = $1`, [c.entidad_id], ); diff --git a/apps/api/src/services/reportes.service.ts b/apps/api/src/services/reportes.service.ts index 29ca951..878e319 100644 --- a/apps/api/src/services/reportes.service.ts +++ b/apps/api/src/services/reportes.service.ts @@ -94,12 +94,12 @@ export async function getFlujoEfectivo( contribuyenteId?: string | null, ): Promise { const VIGENTE = `status NOT IN ('Cancelado', '0')`; - const RANGO = `fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day')`; - const RANGO_PAGO = `fecha_pago_p >= $1::date AND fecha_pago_p < ($2::date + interval '1 day')`; + const RANGO = `COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day')`; + const RANGO_PAGO = `(fecha_pago_p - interval '1 hour') >= $1::date AND (fecha_pago_p - interval '1 hour') < ($2::date + interval '1 day')`; const { esEmisor, esReceptor } = await resolveEmisorReceptor(pool, contribuyenteId); const { rows: entradasPUE } = await pool.query(` - SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total + SELECT TO_CHAR(COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'), 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total FROM cfdis WHERE ${esEmisor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE' AND ${VIGENTE} AND ${RANGO} @@ -107,7 +107,7 @@ export async function getFlujoEfectivo( `, [fechaInicio, fechaFin]); const { rows: entradasPago } = await pool.query(` - SELECT TO_CHAR(fecha_pago_p, 'YYYY-MM') as mes, COALESCE(SUM(monto_pago_mxn), 0) as total + SELECT TO_CHAR(fecha_pago_p - interval '1 hour', 'YYYY-MM') as mes, COALESCE(SUM(monto_pago_mxn), 0) as total FROM cfdis WHERE ${esEmisor} AND tipo_comprobante = 'P' AND ${VIGENTE} AND ${RANGO_PAGO} @@ -115,7 +115,7 @@ export async function getFlujoEfectivo( `, [fechaInicio, fechaFin]); const { rows: entradasNC } = await pool.query(` - SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total + SELECT TO_CHAR(COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'), 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total FROM cfdis WHERE ${esEmisor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE' AND COALESCE(cfdi_tipo_relacion, '') <> '07' @@ -124,7 +124,7 @@ export async function getFlujoEfectivo( `, [fechaInicio, fechaFin]); const { rows: salidasPUE } = await pool.query(` - SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total + SELECT TO_CHAR(COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'), 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total FROM cfdis WHERE ${esReceptor} AND tipo_comprobante = 'I' AND metodo_pago = 'PUE' AND ${VIGENTE} AND ${RANGO} @@ -132,7 +132,7 @@ export async function getFlujoEfectivo( `, [fechaInicio, fechaFin]); const { rows: salidasPago } = await pool.query(` - SELECT TO_CHAR(fecha_pago_p, 'YYYY-MM') as mes, COALESCE(SUM(monto_pago_mxn), 0) as total + SELECT TO_CHAR(fecha_pago_p - interval '1 hour', 'YYYY-MM') as mes, COALESCE(SUM(monto_pago_mxn), 0) as total FROM cfdis WHERE ${esReceptor} AND tipo_comprobante = 'P' AND ${VIGENTE} AND ${RANGO_PAGO} @@ -140,7 +140,7 @@ export async function getFlujoEfectivo( `, [fechaInicio, fechaFin]); const { rows: salidasNC } = await pool.query(` - SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total + SELECT TO_CHAR(COALESCE(fecha_efectiva, fecha_emision - interval '1 hour'), 'YYYY-MM') as mes, COALESCE(SUM(total_mxn), 0) as total FROM cfdis WHERE ${esReceptor} AND tipo_comprobante = 'E' AND metodo_pago = 'PUE' AND COALESCE(cfdi_tipo_relacion, '') <> '07' @@ -187,8 +187,8 @@ async function calcularFlujoPorMes(pool: Pool, año: number, contribuyenteId?: s const VIGENTE = `status NOT IN ('Cancelado', '0')`; const fi = `${año}-01-01`; const ff = `${año}-12-31`; - const RANGO = `fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day')`; - const RANGO_PAGO = `fecha_pago_p >= $1::date AND fecha_pago_p < ($2::date + interval '1 day')`; + const RANGO = `COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day')`; + const RANGO_PAGO = `(fecha_pago_p - interval '1 hour') >= $1::date AND (fecha_pago_p - interval '1 hour') < ($2::date + interval '1 day')`; const { esEmisor, esReceptor } = await resolveEmisorReceptor(pool, contribuyenteId); const q = async (lado: 'EMITIDO' | 'RECIBIDO', tc: string, campo: string, mp?: string) => { @@ -198,7 +198,7 @@ async function calcularFlujoPorMes(pool: Pool, año: number, contribuyenteId?: s const noAnticipo = tc === 'E' ? `AND COALESCE(cfdi_tipo_relacion, '') <> '07'` : ''; const ladoCond = lado === 'EMITIDO' ? esEmisor : esReceptor; const { rows } = await pool.query(` - SELECT EXTRACT(MONTH FROM ${fechaCol})::int as mes, COALESCE(SUM(${campo}), 0) as total + SELECT EXTRACT(MONTH FROM COALESCE(fecha_efectiva, ${fechaCol} - interval '1 hour'))::int as mes, COALESCE(SUM(${campo}), 0) as total FROM cfdis WHERE ${ladoCond} AND tipo_comprobante = '${tc}' ${mpF} ${noAnticipo} AND ${VIGENTE} AND ${rango} GROUP BY mes @@ -277,7 +277,7 @@ export async function getConcentradoRfc( COUNT(*)::int as "cantidadCfdis" FROM cfdis WHERE ${esEmisor} AND tipo_comprobante = 'I' AND status NOT IN ('Cancelado', '0') - AND fecha_emision BETWEEN $1::date AND $2::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') BETWEEN $1::date AND $2::date GROUP BY rfc_receptor, nombre_receptor ORDER BY "totalFacturado" DESC `, [fechaInicio, fechaFin]); @@ -298,7 +298,7 @@ export async function getConcentradoRfc( COUNT(*)::int as "cantidadCfdis" FROM cfdis WHERE ${esReceptor} AND tipo_comprobante = 'I' AND status NOT IN ('Cancelado', '0') - AND fecha_emision BETWEEN $1::date AND $2::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') BETWEEN $1::date AND $2::date GROUP BY rfc_emisor, nombre_emisor ORDER BY "totalFacturado" DESC `, [fechaInicio, fechaFin]); @@ -338,8 +338,8 @@ export async function getCuentasXPagar( FROM cfdis WHERE ${esReceptor} AND tipo_comprobante = 'I' AND metodo_pago = 'PPD' AND status NOT IN ('Cancelado', '0') - AND fecha_emision >= $1::date - AND fecha_emision < ($2::date + interval '1 day') + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day') AND COALESCE(saldo_pendiente_mxn, total_mxn) > 0.01 ${regimenFilter} GROUP BY rfc_emisor, nombre_emisor @@ -365,7 +365,7 @@ export async function getCuentasXPagar( // ───────────────────────────────────────────────────────────────────────────── const VIGENTE_ER = `status NOT IN ('Cancelado', '0')`; -const RANGO_FECHA = `fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day')`; +const RANGO_FECHA = `COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day')`; const BASE_MONTO = `COALESCE(subtotal_mxn, 0) - COALESCE(descuento_mxn, 0)`; function sameDateLastYear(dateStr: string): string { @@ -842,8 +842,8 @@ export async function getCuentasXCobrar( FROM cfdis WHERE ${esEmisor} AND tipo_comprobante = 'I' AND metodo_pago = 'PPD' AND status NOT IN ('Cancelado', '0') - AND fecha_emision >= $1::date - AND fecha_emision < ($2::date + interval '1 day') + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $1::date + AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') < ($2::date + interval '1 day') AND COALESCE(saldo_pendiente_mxn, total_mxn) > 0.01 ${regimenFilter} GROUP BY rfc_receptor, nombre_receptor diff --git a/apps/api/src/services/sat/sat-parser.service.ts b/apps/api/src/services/sat/sat-parser.service.ts index f0c81a1..175cd3e 100644 --- a/apps/api/src/services/sat/sat-parser.service.ts +++ b/apps/api/src/services/sat/sat-parser.service.ts @@ -69,6 +69,11 @@ interface CfdiParsed { cfdisRelacionados: string | null; conceptos: ConceptoParsed[]; xmlOriginal: string; + + // Factura global (InformacionGlobal) + periodicidad: string | null; + mesesGlobal: string | null; + añoGlobal: string | null; } interface ConceptoParsed { @@ -569,6 +574,9 @@ export function parseXml(xmlContent: string, downloadType: 'emitidos' | 'recibid ...nominaData, conceptos: extractConceptos(comprobante), xmlOriginal: xmlContent, + periodicidad: comprobante.InformacionGlobal?.['@_Periodicidad'] || null, + mesesGlobal: comprobante.InformacionGlobal?.['@_Meses'] || null, + añoGlobal: comprobante.InformacionGlobal?.['@_Año'] || null, }; if (!cfdi.uuid) { diff --git a/apps/api/src/services/sat/sat.service.ts b/apps/api/src/services/sat/sat.service.ts index be99237..3aa0ae2 100644 --- a/apps/api/src/services/sat/sat.service.ts +++ b/apps/api/src/services/sat/sat.service.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as path from 'path'; const POLL_INTERVAL_MS = 60000; // 60 segundos -const MAX_POLL_ATTEMPTS = 45; // 45 minutos máximo (45 × 60s) +const MAX_POLL_ATTEMPTS = 500; // ~8 horas máximo para syncs iniciales grandes const YEARS_TO_SYNC = 6; // SAT solo permite descargar últimos 6 años /** @@ -121,6 +121,35 @@ async function getOrCreateRfc(pool: Pool, rfc: string, razonSocial: string | nul return rows[0].id; } +/** + * Calcula la fecha efectiva de un CFDI para métricas. + * Si tiene InformacionGlobal, usa el año/mes declarado. + * Para bimestral (periodicidad 05), convierte el código 13-18 a mes 2-12. + */ +function calcFechaEfectiva(cfdi: CfdiParsed): Date | null { + if (!cfdi.añoGlobal || !cfdi.mesesGlobal) { + return null; + } + const anio = parseInt(cfdi.añoGlobal, 10); + if (isNaN(anio)) return null; + + const mesesStr = cfdi.mesesGlobal; + const mesesParts = mesesStr.split(',').map((s: string) => s.trim()); + const ultimoMesStr = mesesParts[mesesParts.length - 1]; + let mes = parseInt(ultimoMesStr, 10); + if (isNaN(mes)) return null; + + // Bimestral: códigos 13-18 → meses 2,4,6,8,10,12 + if (cfdi.periodicidad === '05') { + if (mes >= 13 && mes <= 18) { + mes = (mes - 12) * 2; + } + } + + if (mes < 1 || mes > 12) return null; + return new Date(anio, mes - 1, 1); +} + /** * Guarda los XMLs extraídos del ZIP en disco para respaldo */ @@ -212,6 +241,10 @@ async function saveCfdis( cfdi.subsidioCausado, m(cfdi.subsidioCausado), cfdi.regimenFiscalEmisor, cfdi.regimenFiscalReceptor, cfdi.codigoPostalReceptor, + cfdi.periodicidad, + cfdi.mesesGlobal, + cfdi.añoGlobal, + calcFechaEfectiva(cfdi), cfdi.xmlOriginal, cfdi.cfdiTipoRelacion, cfdi.cfdisRelacionados, jobId, @@ -261,16 +294,17 @@ async function saveCfdis( subsidio_causado=$78, subsidio_causado_mxn=$79, regimen_fiscal_emisor=$80, regimen_fiscal_receptor=$81, codigo_postal_receptor=$82, - xml_original=$83, - cfdi_tipo_relacion=$84, cfdis_relacionados=$85, - last_sat_sync=NOW(), sat_sync_job_id=$86::uuid, + periodicidad=$83, meses_global=$84, año_global=$85, fecha_efectiva=$86, + xml_original=$87, + cfdi_tipo_relacion=$88, cfdis_relacionados=$89, + last_sat_sync=NOW(), sat_sync_job_id=$90::uuid, actualizado_en=NOW() WHERE uuid = $1`, [cfdi.uuid, ...vals] ); // Re-insert conceptos for updated CFDI await pool.query(`DELETE FROM cfdi_conceptos WHERE cfdi_id = $1`, [existing[0].id]); - await saveConceptos(pool, existing[0].id, cfdi); + await saveConceptosWithRetry(pool, existing[0].id, cfdi); updated++; } else { // $1-$83 = data fields (year..cfdis_relacionados), $84 = jobId, $85 = contribuyente_id @@ -310,6 +344,7 @@ async function saveCfdis( subsidio_causado, subsidio_causado_mxn, regimen_fiscal_emisor, regimen_fiscal_receptor, codigo_postal_receptor, + periodicidad, meses_global, año_global, fecha_efectiva, xml_original, cfdi_tipo_relacion, cfdis_relacionados, source, sat_sync_job_id, last_sat_sync, contribuyente_id @@ -321,7 +356,7 @@ async function saveCfdis( ); // Get the inserted cfdi id and save conceptos const { rows: [newRow] } = await pool.query(`SELECT id FROM cfdis WHERE uuid = $1`, [cfdi.uuid]); - if (newRow) await saveConceptos(pool, newRow.id, cfdi); + if (newRow) await saveConceptosWithRetry(pool, newRow.id, cfdi); inserted++; } // Marcar el mes para recompute de métricas pre-calculadas. Para tipo P @@ -404,6 +439,26 @@ async function saveConceptos(pool: Pool, cfdiId: number, cfdi: CfdiParsed): Prom } } +/** Reintenta saveConceptos con backoff exponencial para tolerar errores transitorios. */ +async function saveConceptosWithRetry(pool: Pool, cfdiId: number, cfdi: CfdiParsed, maxRetries = 3): Promise { + let lastError: any; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await saveConceptos(pool, cfdiId, cfdi); + return; + } catch (err: any) { + lastError = err; + if (attempt < maxRetries) { + const delay = 500 * attempt; + console.warn(`[SAT] saveConceptos falló (intento ${attempt}/${maxRetries}) para CFDI ${cfdi.uuid}, reintentando en ${delay}ms...`); + await new Promise(r => setTimeout(r, delay)); + } + } + } + console.error(`[SAT] saveConceptos falló definitivamente después de ${maxRetries} intentos para CFDI ${cfdi.uuid}:`, lastError?.message || lastError); + throw lastError; +} + /** * Guarda/actualiza CFDIs desde metadata del SAT. * - Si el CFDI no existe: inserta con datos básicos de metadata (sin XML). @@ -770,6 +825,26 @@ async function determineChunkMonths( fechaInicio: Date, fechaFin: Date, ): Promise { + // Si el job previo del mismo tenant/contribuyente ya tenía chunks, + // inferimos que el volumen es alto y usamos 6 meses directamente + // para evitar el sondeo lento del SAT. + const previousJob = await prisma.satSyncJob.findFirst({ + where: { + tenantId: ctx.tenantId, + contribuyenteId: ctx.contribuyenteId ?? null, + id: { not: jobId }, + status: 'completed', + cfdisFound: { gt: 0 }, + }, + orderBy: { createdAt: 'desc' }, + select: { satRequestIds: true, cfdisFound: true }, + }); + if (previousJob?.satRequestIds && Object.keys(previousJob.satRequestIds as Record).length > 0) { + const chunkMonths = (previousJob.cfdisFound || 0) > 15_000 ? 3 : 6; + console.log(`[SAT] Reutilizando estrategia de job previo (${previousJob.cfdisFound} CFDIs) → bloques de ${chunkMonths} meses`); + return chunkMonths; + } + const THRESHOLD = 15_000; let totalCfdis = 0; diff --git a/apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx b/apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx index 41200ca..d184a30 100644 --- a/apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx +++ b/apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; import { Card, CardContent, Button, SortableHeader } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; import { exportToExcel } from '@/lib/export-excel'; import { useTableSort } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; @@ -27,8 +27,8 @@ const EXCEL_COLUMNS = [ function prepareRows(data: any[]) { return data.map((c) => ({ ...c, - _fechaEmision: new Date(c.fechaEmision).toLocaleDateString('es-MX'), - _fechaCancelacion: c.fechaCancelacion ? new Date(c.fechaCancelacion).toLocaleDateString('es-MX') : '', + _fechaEmision: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'), + _fechaCancelacion: c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).toLocaleDateString('es-MX') : '', _totalMxn: Number(c.totalMxn || 0), })); } @@ -50,8 +50,8 @@ export default function CancelacionesPeriodoAnteriorPage() { const { sortedData, toggleSort, getSortIndicator } = useTableSort( data, { - fecha: (c) => new Date(c.fechaEmision).getTime(), - cancelacion: (c: any) => c.fechaCancelacion ? new Date(c.fechaCancelacion).getTime() : 0, + fecha: (c) => toCfdiDate(c.fechaEmision).getTime(), + cancelacion: (c: any) => c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).getTime() : 0, total: (c) => Number(c.totalMxn || 0), }, 'cancelacion', @@ -97,8 +97,8 @@ export default function CancelacionesPeriodoAnteriorPage() { {(sortedData || []).map((cfdi: any) => ( {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')} - {cfdi.fechaCancelacion ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '-'} + {toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')} + {cfdi.fechaCancelacion ? toCfdiDate(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '-'} {cfdi.rfcEmisor} {cfdi.rfcReceptor} {formatCurrency(Number(cfdi.totalMxn))} diff --git a/apps/web/app/(dashboard)/alertas/cancelaciones/page.tsx b/apps/web/app/(dashboard)/alertas/cancelaciones/page.tsx index 49afd7c..6673ad3 100644 --- a/apps/web/app/(dashboard)/alertas/cancelaciones/page.tsx +++ b/apps/web/app/(dashboard)/alertas/cancelaciones/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; import { Card, CardContent, Button, SortableHeader } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; import { exportToExcel } from '@/lib/export-excel'; import { useTableSort } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; @@ -26,8 +26,8 @@ const EXCEL_COLUMNS = [ function prepareRows(data: any[]) { return data.map((c) => ({ ...c, - _fechaEmision: new Date(c.fechaEmision).toLocaleDateString('es-MX'), - _fechaCancelacion: c.fechaCancelacion ? new Date(c.fechaCancelacion).toLocaleDateString('es-MX') : '', + _fechaEmision: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'), + _fechaCancelacion: c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).toLocaleDateString('es-MX') : '', _totalMxn: Number(c.totalMxn || 0), })); } @@ -46,8 +46,8 @@ export default function CancelacionesPage() { const { sortedData, toggleSort, getSortIndicator } = useTableSort( data, { - fecha: (c) => new Date(c.fechaEmision).getTime(), - cancelacion: (c: any) => c.fechaCancelacion ? new Date(c.fechaCancelacion).getTime() : 0, + fecha: (c) => toCfdiDate(c.fechaEmision).getTime(), + cancelacion: (c: any) => c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).getTime() : 0, total: (c) => Number(c.totalMxn || 0), }, 'cancelacion', @@ -91,8 +91,8 @@ export default function CancelacionesPage() { {(sortedData || []).map((cfdi: any) => ( {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')} - {cfdi.fechaCancelacion ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '-'} + {toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')} + {cfdi.fechaCancelacion ? toCfdiDate(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '-'} {cfdi.rfcEmisor} {cfdi.rfcReceptor} {formatCurrency(Number(cfdi.totalMxn))} diff --git a/apps/web/app/(dashboard)/alertas/discrepancia-regimen/page.tsx b/apps/web/app/(dashboard)/alertas/discrepancia-regimen/page.tsx index 61d76e5..3b40f9f 100644 --- a/apps/web/app/(dashboard)/alertas/discrepancia-regimen/page.tsx +++ b/apps/web/app/(dashboard)/alertas/discrepancia-regimen/page.tsx @@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; import { Card, CardContent, CardHeader, CardTitle, Button, SortableHeader, Input } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; import { exportToExcel } from '@/lib/export-excel'; import { useTableSort } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; @@ -29,7 +29,7 @@ const EXCEL_COLUMNS = [ function prepareRows(data: any[]) { return data.map((c) => ({ ...c, - _fecha: new Date(c.fechaEmision).toLocaleDateString('es-MX'), + _fecha: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'), _totalMxn: Number(c.totalMxn || 0), regimenReceptor: c.regimenReceptor || c.regimenFiscalReceptor || '', })); @@ -91,10 +91,10 @@ export default function DiscrepanciaRegimenPage() { let filtered = data; if (fechaDesde) { - filtered = filtered.filter(c => c.fechaEmision >= fechaDesde); + filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() >= fechaDesde); } if (fechaHasta) { - filtered = filtered.filter(c => c.fechaEmision <= fechaHasta + 'T23:59:59'); + filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() <= fechaHasta + 'T23:59:59'); } if (regimenFilter) { filtered = filtered.filter((c: any) => (c.regimenReceptor || c.regimenFiscalReceptor) === regimenFilter); @@ -106,7 +106,7 @@ export default function DiscrepanciaRegimenPage() { const { sortedData, toggleSort, getSortIndicator } = useTableSort( visibleData, { - fecha: (c) => new Date(c.fechaEmision).getTime(), + fecha: (c) => toCfdiDate(c.fechaEmision).getTime(), total: (c) => Number(c.totalMxn || 0), }, 'fecha', @@ -311,7 +311,7 @@ export default function DiscrepanciaRegimenPage() { {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')} + {toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')} {cfdi.rfcEmisor} {cfdi.nombreEmisor} {cfdi.regimenReceptor} diff --git a/apps/web/app/(dashboard)/alertas/efectivo/page.tsx b/apps/web/app/(dashboard)/alertas/efectivo/page.tsx index 14fea33..5decf81 100644 --- a/apps/web/app/(dashboard)/alertas/efectivo/page.tsx +++ b/apps/web/app/(dashboard)/alertas/efectivo/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; import { Card, CardContent, Button, SortableHeader } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; import { exportToExcel } from '@/lib/export-excel'; import { useTableSort } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; @@ -26,7 +26,7 @@ const EXCEL_COLUMNS = [ function prepareRows(data: any[]) { return data.map((c) => ({ ...c, - _fecha: new Date(c.fechaEmision).toLocaleDateString('es-MX'), + _fecha: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'), _totalMxn: Number(c.totalMxn || 0), })); } @@ -45,7 +45,7 @@ export default function EfectivoPage() { const { sortedData, toggleSort, getSortIndicator } = useTableSort( data, { - fecha: (c) => new Date(c.fechaEmision).getTime(), + fecha: (c) => toCfdiDate(c.fechaEmision).getTime(), total: (c) => Number(c.totalMxn || 0), }, 'fecha', @@ -89,7 +89,7 @@ export default function EfectivoPage() { {(sortedData || []).map((cfdi: any) => ( {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')} + {toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')} {cfdi.rfcEmisor} {cfdi.nombreEmisor} {cfdi.rfcReceptor} diff --git a/apps/web/app/(dashboard)/alertas/tipo-relacion-sospechosa/page.tsx b/apps/web/app/(dashboard)/alertas/tipo-relacion-sospechosa/page.tsx index 183eb25..f2b63dc 100644 --- a/apps/web/app/(dashboard)/alertas/tipo-relacion-sospechosa/page.tsx +++ b/apps/web/app/(dashboard)/alertas/tipo-relacion-sospechosa/page.tsx @@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { DashboardShell } from '@/components/layouts/dashboard-shell'; import { Card, CardContent, CardHeader, CardTitle, Button, SortableHeader, Input } from '@horux/shared-ui'; import { apiClient } from '@/lib/api/client'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; import { exportToExcel } from '@/lib/export-excel'; import { useTableSort } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; @@ -30,7 +30,7 @@ const EXCEL_COLUMNS = [ function prepareRows(data: any[]) { return data.map((c) => ({ ...c, - _fecha: new Date(c.fechaEmision).toLocaleDateString('es-MX'), + _fecha: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'), _totalMxn: Number(c.totalMxn || 0), })); } @@ -83,8 +83,8 @@ export default function TipoRelacionSospechosaPage() { const visibleData = useMemo(() => { if (!data) return []; let filtered = data; - if (fechaDesde) filtered = filtered.filter(c => c.fechaEmision >= fechaDesde); - if (fechaHasta) filtered = filtered.filter(c => c.fechaEmision <= fechaHasta + 'T23:59:59'); + if (fechaDesde) filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() >= fechaDesde); + if (fechaHasta) filtered = filtered.filter(c => toCfdiDate(c.fechaEmision).toISOString() <= fechaHasta + 'T23:59:59'); if (tipoRelFilter) filtered = filtered.filter((c: any) => c.cfdiTipoRelacion === tipoRelFilter); return filtered; }, [data, fechaDesde, fechaHasta, tipoRelFilter]); @@ -92,7 +92,7 @@ export default function TipoRelacionSospechosaPage() { const { sortedData, toggleSort, getSortIndicator } = useTableSort( visibleData, { - fecha: (c) => new Date(c.fechaEmision).getTime(), + fecha: (c) => toCfdiDate(c.fechaEmision).getTime(), total: (c) => Number(c.totalMxn || 0), }, 'fecha', @@ -296,7 +296,7 @@ export default function TipoRelacionSospechosaPage() { {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaEmision).toLocaleDateString('es-MX')} + {toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')}
{cfdi.rfcEmisor}
{cfdi.nombreEmisor}
diff --git a/apps/web/app/(dashboard)/cfdi/page.tsx b/apps/web/app/(dashboard)/cfdi/page.tsx index 14e839b..2dd750d 100644 --- a/apps/web/app/(dashboard)/cfdi/page.tsx +++ b/apps/web/app/(dashboard)/cfdi/page.tsx @@ -421,7 +421,7 @@ export default function CfdiPage() { } const exportData = allRows.map(cfdi => ({ - 'Fecha Emisión': new Date(cfdi.fechaEmision).toLocaleDateString('es-MX'), + 'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision), 'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante), 'Uso CFDI': (cfdi as any).usoCfdi || '', 'Serie': cfdi.serie || '', @@ -442,9 +442,7 @@ export default function CfdiPage() { // vacío en Excel para no confundir "0 = pagado" con "no aplica". 'Saldo Pendiente': cfdi.saldoPendienteMxn ?? '', 'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado', - 'Fecha Cancelación': cfdi.fechaCancelacion - ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') - : '', + 'Fecha Cancelación': formatCfdiDate(cfdi.fechaCancelacion), 'UUID': cfdi.uuid, })); @@ -509,7 +507,7 @@ export default function CfdiPage() { if (key.endsWith('_mxn') || key === 'id' || key === 'cfdi_id') continue; // Formatear fecha si aplica if (key === 'fechaEmision' && typeof val === 'string') { - out['Fecha Emisión'] = new Date(val).toLocaleDateString('es-MX'); + out['Fecha Emisión'] = formatCfdiDate(val); } else { out[key] = val; } @@ -539,7 +537,7 @@ export default function CfdiPage() { const exportSingleCfdiToExcel = (cfdi: Cfdi) => { const row = { - 'Fecha Emisión': new Date(cfdi.fechaEmision).toLocaleDateString('es-MX'), + 'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision), 'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante), 'Uso CFDI': (cfdi as any).usoCfdi || '', 'Serie': cfdi.serie || '', @@ -560,9 +558,7 @@ export default function CfdiPage() { // vacío en Excel para no confundir "0 = pagado" con "no aplica". 'Saldo Pendiente': cfdi.saldoPendienteMxn ?? '', 'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado', - 'Fecha Cancelación': cfdi.fechaCancelacion - ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') - : '', + 'Fecha Cancelación': formatCfdiDate(cfdi.fechaCancelacion), 'UUID': cfdi.uuid, }; @@ -935,12 +931,22 @@ export default function CfdiPage() { currency: 'MXN', }).format(value); - const formatDate = (dateString: string) => - new Date(dateString).toLocaleDateString('es-MX', { + const formatDate = (dateString: string) => { + const d = new Date(dateString); + d.setHours(d.getHours() - 1); + return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric', }); + }; + + const formatCfdiDate = (dateString: string | null | undefined) => { + if (!dateString) return '-'; + const d = new Date(dateString); + d.setHours(d.getHours() - 1); + return d.toLocaleDateString('es-MX'); + }; const generateUUID = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { @@ -1697,7 +1703,7 @@ export default function CfdiPage() { {conceptosQuery.data.data.map((row, idx) => ( - {new Date(row.fechaEmision).toLocaleDateString('es-MX')} + {formatCfdiDate(row.fechaEmision)} {row.uuid?.substring(0, 8) || '-'} {row.clave_prod_serv || '-'} {row.descripcion} diff --git a/apps/web/app/(dashboard)/conciliacion/page.tsx b/apps/web/app/(dashboard)/conciliacion/page.tsx index ffe6907..ea026e9 100644 --- a/apps/web/app/(dashboard)/conciliacion/page.tsx +++ b/apps/web/app/(dashboard)/conciliacion/page.tsx @@ -7,9 +7,9 @@ import { useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard'; import { PeriodSelector, RegimenSelector } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; import { Header } from '@/components/layouts/header'; -import { Card, CardContent, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Input } from '@horux/shared-ui'; +import { Card, CardContent, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Input, Label, Popover, PopoverTrigger, PopoverContent } from '@horux/shared-ui'; import { useAuthStore } from '@/stores/auth-store'; -import { formatCurrency } from '@/lib/utils'; +import { formatCurrency, toCfdiDate } from '@/lib/utils'; function formatCurrencyConciliacion(value: number): string { return new Intl.NumberFormat('es-MX', { @@ -20,7 +20,7 @@ function formatCurrencyConciliacion(value: number): string { }).format(value); } import { exportToExcel } from '@/lib/export-excel'; -import { Eye, Download, X, CheckCircle } from 'lucide-react'; +import { Eye, Download, X, CheckCircle, Search, ArrowUpDown, Filter } from 'lucide-react'; function getMonthRange(year: number, month: number) { const start = `${year}-${String(month).padStart(2, '0')}-01`; @@ -42,6 +42,20 @@ export default function ConciliacionPage() { const [bancoId, setBancoId] = useState(''); const [selectedCfdi, setSelectedCfdi] = useState(null); + // Ordenación — Por conciliar + const [sortPendientes, setSortPendientes] = useState<{ field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null>(null); + + // Ordenación — Conciliadas + const [sortConciliadas, setSortConciliadas] = useState<{ field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null>(null); + + // Filtros por columna — Por conciliar + const [filtersPendientes, setFiltersPendientes] = useState({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); + const [openFilterPendientes, setOpenFilterPendientes] = useState(null); + + // Filtros por columna — Conciliadas + const [filtersConciliadas, setFiltersConciliadas] = useState({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); + const [openFilterConciliadas, setOpenFilterConciliadas] = useState(null); + const { user } = useAuthStore(); const isVisor = user?.role === 'visor'; @@ -66,9 +80,15 @@ export default function ConciliacionPage() { const montoConciliado = conciliadas.reduce((s, c) => s + getMonto(c), 0); const montoPendiente = pendientes.reduce((s, c) => s + getMonto(c), 0); - // Reset selection on tab/filter change + // Reset selection + ordenación + filtros on tab/filter change useEffect(() => { setSelected(new Set()); + setSortPendientes(null); + setSortConciliadas(null); + setFiltersPendientes({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); + setOpenFilterPendientes(null); + setFiltersConciliadas({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); + setOpenFilterConciliadas(null); }, [activeTab, fechaInicio, fechaFin, regimenSeleccionado]); // Handlers @@ -85,10 +105,10 @@ export default function ConciliacionPage() { }; const toggleSelectAll = () => { - if (selected.size === pendientes.length && pendientes.length > 0) { + if (selected.size === pendientesOrdenados.length && pendientesOrdenados.length > 0) { setSelected(new Set()); } else { - setSelected(new Set(pendientes.map((c) => c.id))); + setSelected(new Set(pendientesOrdenados.map((c) => c.id))); } }; @@ -117,12 +137,100 @@ export default function ConciliacionPage() { } }; + function matchesColumnFilters(c: any, filters: { rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string }) { + const rfcEmisorMatch = !filters.rfcEmisor || (c.rfcEmisor || '').toLowerCase().includes(filters.rfcEmisor.toLowerCase()); + const nombreEmisorMatch = !filters.nombreEmisor || (c.nombreEmisor || '').toLowerCase().includes(filters.nombreEmisor.toLowerCase()); + const rfcReceptorMatch = !filters.rfcReceptor || (c.rfcReceptor || '').toLowerCase().includes(filters.rfcReceptor.toLowerCase()); + const nombreReceptorMatch = !filters.nombreReceptor || (c.nombreReceptor || '').toLowerCase().includes(filters.nombreReceptor.toLowerCase()); + return rfcEmisorMatch && nombreEmisorMatch && rfcReceptorMatch && nombreReceptorMatch; + } + + function sortCfdis(list: any[], sort: { field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null) { + if (!sort) return list; + const sorted = [...list].sort((a, b) => { + if (sort.field === 'fecha') { + const da = toCfdiDate(a.fechaPagoP || a.fechaEmision).getTime(); + const db = toCfdiDate(b.fechaPagoP || b.fechaEmision).getTime(); + return sort.dir === 'asc' ? da - db : db - da; + } + if (sort.field === 'total') { + const ta = getMonto(a); + const tb = getMonto(b); + return sort.dir === 'asc' ? ta - tb : tb - ta; + } + return 0; + }); + return sorted; + } + + function FilterHeader({ + label, + filterKey, + filters, + setFilters, + openFilter, + setOpenFilter, + }: { + label: string; + filterKey: string; + filters: { rfcEmisor: string; nombreEmisor: string; rfcReceptor: string; nombreReceptor: string }; + setFilters: React.Dispatch>; + openFilter: string | null; + setOpenFilter: (v: string | null) => void; + }) { + const hasFilter = !!(filters as any)[filterKey]; + return ( +
+ {label} + setOpenFilter(open ? filterKey : null)}> + + + + +
+

Filtrar por {label}

+
+ + setFilters((prev: any) => ({ ...prev, [filterKey]: e.target.value }))} + /> +
+
+ + {hasFilter && ( + + )} +
+
+
+
+
+ ); + } + + const pendientesOrdenados = sortCfdis( + pendientes.filter((c) => matchesColumnFilters(c, filtersPendientes)), + sortPendientes + ); + const conciliadasOrdenadas = sortCfdis( + conciliadas.filter((c) => matchesColumnFilters(c, filtersConciliadas)), + sortConciliadas + ); + const handleExport = () => { - if (!cfdis?.length) return; + const allVisible = [...pendientesOrdenados, ...conciliadasOrdenadas]; + if (!allVisible.length) return; exportToExcel( - cfdis.map((c) => ({ + allVisible.map((c) => ({ ...c, - _fecha: new Date(c.fechaPagoP || c.fechaEmision).toLocaleDateString('es-MX'), + _fecha: toCfdiDate(c.fechaPagoP || c.fechaEmision).toLocaleDateString('es-MX'), _totalMxn: getMonto(c), _estado: c.conciliado === 'true' ? 'Conciliado' : 'Pendiente', _fechaPago: c.conciliacion?.fechaDePago || '', @@ -212,8 +320,8 @@ export default function ConciliacionPage() { {/* Por conciliar */} -

Por conciliar ({pendientes.length})

- {pendientes.length === 0 ? ( +

Por conciliar ({pendientesOrdenados.length})

+ {pendientesOrdenados.length === 0 ? (

No hay CFDIs pendientes de conciliar

@@ -221,31 +329,35 @@ export default function ConciliacionPage() {
- + {!isVisor && ( )} - - - - - - + + + + + + - {pendientes.map((cfdi) => ( + {pendientesOrdenados.map((cfdi) => ( {!isVisor && ( )} - - - - + - - + - - - +
0 + selected.size === pendientesOrdenados.length && pendientesOrdenados.length > 0 } onChange={toggleSelectAll} /> UUIDFechaRFC EmisorNombre EmisorRFC ReceptorNombre ReceptorTotal MXN setSortPendientes(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}> + Fecha + setSortPendientes(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}> + Total MXN + M. Pago
@@ -256,25 +368,25 @@ export default function ConciliacionPage() { /> + {cfdi.uuid?.substring(0, 8)} - {new Date(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')} + + {toCfdiDate(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')} {cfdi.rfcEmisor} + {cfdi.rfcEmisor} {cfdi.nombreEmisor} {cfdi.rfcReceptor} + {cfdi.rfcReceptor} {cfdi.nombreReceptor} + {formatCurrencyConciliacion(getMonto(cfdi))} {cfdi.metodoPago || '-'} + {cfdi.metodoPago || '-'}