From bda0a4e212006abc6f1b86b11150e5b85de5cc89 Mon Sep 17 00:00:00 2001 From: Horux Dev Date: Sat, 16 May 2026 14:45:00 +0000 Subject: [PATCH] feat(cfdi): agrega C.P. receptor, regimen receptor, no_identificacion, tipo_relacion y CFDIs relacionados al visualizador Backend: - Migracion 044: codigo_postal_receptor VARCHAR(5) + indice - sat-parser: extrae DomicilioFiscalReceptor - sat.service: persiste codigo_postal_receptor en INSERT/UPDATE - cfdi.service: incluye codigo_postal_receptor en CFDI_SELECT - shared/types: codigoPostalReceptor en interfaz Cfdi Frontend: - cfdi-invoice: tarjeta receptor con C.P. y regimen (con descripciones) - cfdi-invoice: seccion CFDI Relacionado (tipo + UUIDs) - cfdi-invoice: columna No. Identificacion en tabla de conceptos - cfdi-viewer-modal: mapea noIdentificacion desde DB y XML --- .../tenant/044_cfdi_cp_receptor.sql | 2 + apps/api/src/services/cfdi.service.ts | 1 + .../src/services/sat/sat-parser.service.ts | 2 + apps/api/src/services/sat/sat.service.ts | 9 +- apps/web/components/cfdi/cfdi-invoice.tsx | 91 ++++++++++++++++++- .../web/components/cfdi/cfdi-viewer-modal.tsx | 3 + docs/CAMBIOS-2026-05-09.md | 39 ++++++++ packages/shared/src/types/cfdi.ts | 1 + 8 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql diff --git a/apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql b/apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql new file mode 100644 index 0000000..a4dabc7 --- /dev/null +++ b/apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql @@ -0,0 +1,2 @@ +-- Agregar código postal del receptor al CFDI (extraído del XML durante sync SAT) +ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS codigo_postal_receptor VARCHAR(5); diff --git a/apps/api/src/services/cfdi.service.ts b/apps/api/src/services/cfdi.service.ts index 207c9b0..41957fc 100644 --- a/apps/api/src/services/cfdi.service.ts +++ b/apps/api/src/services/cfdi.service.ts @@ -60,6 +60,7 @@ const CFDI_SELECT = ` conciliado, regimen_fiscal_emisor as "regimenFiscalEmisor", regimen_fiscal_receptor as "regimenFiscalReceptor", + codigo_postal_receptor as "codigoPostalReceptor", xml_url as "xmlUrl", pdf_url as "pdfUrl", xml_original as "xmlOriginal", cfdi_tipo_relacion as "cfdiTipoRelacion", diff --git a/apps/api/src/services/sat/sat-parser.service.ts b/apps/api/src/services/sat/sat-parser.service.ts index c092de4..f0c81a1 100644 --- a/apps/api/src/services/sat/sat-parser.service.ts +++ b/apps/api/src/services/sat/sat-parser.service.ts @@ -61,6 +61,7 @@ interface CfdiParsed { regimenFiscalEmisor: string | null; regimenFiscalReceptor: string | null; + codigoPostalReceptor: string | null; // CfdiRelacionados a nivel raíz del comprobante (CFDI 4.0). // `cfdiTipoRelacion` — clave SAT (01..07). NULL si no hay relación. // `cfdisRelacionados` — UUIDs pipe-separated. @@ -541,6 +542,7 @@ export function parseXml(xmlContent: string, downloadType: 'emitidos' | 'recibid pac: timbreData.pac, regimenFiscalEmisor: emisor['@_RegimenFiscal'] || null, regimenFiscalReceptor: receptor['@_RegimenFiscalReceptor'] || receptor['@_RegimenFiscal'] || null, + codigoPostalReceptor: receptor['@_DomicilioFiscalReceptor'] || null, cfdiTipoRelacion: relacionesData.tipoRelacion, cfdisRelacionados: relacionesData.uuids, // Impuestos comprobante diff --git a/apps/api/src/services/sat/sat.service.ts b/apps/api/src/services/sat/sat.service.ts index 8a84a02..a2ff9ac 100644 --- a/apps/api/src/services/sat/sat.service.ts +++ b/apps/api/src/services/sat/sat.service.ts @@ -211,6 +211,7 @@ async function saveCfdis( cfdi.otrasDeduccionesNomina, m(cfdi.otrasDeduccionesNomina), cfdi.subsidioCausado, m(cfdi.subsidioCausado), cfdi.regimenFiscalEmisor, cfdi.regimenFiscalReceptor, + cfdi.codigoPostalReceptor, cfdi.xmlOriginal, cfdi.cfdiTipoRelacion, cfdi.cfdisRelacionados, jobId, @@ -259,9 +260,10 @@ async function saveCfdis( otras_deducciones_nomina=$76, otras_deducciones_nomina_mxn=$77, subsidio_causado=$78, subsidio_causado_mxn=$79, regimen_fiscal_emisor=$80, regimen_fiscal_receptor=$81, - xml_original=$82, - cfdi_tipo_relacion=$83, cfdis_relacionados=$84, - last_sat_sync=NOW(), sat_sync_job_id=$85::uuid, + codigo_postal_receptor=$82, + xml_original=$83, + cfdi_tipo_relacion=$84, cfdis_relacionados=$85, + last_sat_sync=NOW(), sat_sync_job_id=$86::uuid, actualizado_en=NOW() WHERE uuid = $1`, [cfdi.uuid, ...vals] @@ -307,6 +309,7 @@ async function saveCfdis( otras_deducciones_nomina, otras_deducciones_nomina_mxn, subsidio_causado, subsidio_causado_mxn, regimen_fiscal_emisor, regimen_fiscal_receptor, + codigo_postal_receptor, xml_original, cfdi_tipo_relacion, cfdis_relacionados, source, sat_sync_job_id, last_sat_sync, contribuyente_id diff --git a/apps/web/components/cfdi/cfdi-invoice.tsx b/apps/web/components/cfdi/cfdi-invoice.tsx index fcaee2b..6695564 100644 --- a/apps/web/components/cfdi/cfdi-invoice.tsx +++ b/apps/web/components/cfdi/cfdi-invoice.tsx @@ -10,6 +10,7 @@ interface CfdiConcepto { importe: number; claveUnidad?: string; claveProdServ?: string; + noIdentificacion?: string; } interface CfdiInvoiceProps { @@ -66,6 +67,38 @@ const metodoPagoLabels: Record = { PPD: 'Pago en parcialidades o diferido', }; +const regimenFiscalLabels: Record = { + '601': 'General de Ley Personas Morales', + '603': 'Personas Morales con Fines no Lucrativos', + '605': 'Sueldos y Salarios e Ingresos Asimilados a Salarios', + '606': 'Arrendamiento', + '608': 'Demás ingresos', + '609': 'Consolidación', + '610': 'Residentes en el Extranjero sin Establecimiento Permanente en México', + '611': 'Ingresos por Dividendos (socios y accionistas)', + '612': 'Personas Físicas con Actividades Empresariales y Profesionales', + '614': 'Ingresos por intereses', + '615': 'Régimen de los ingresos por obtención de premios', + '616': 'Sin obligaciones fiscales', + '620': 'Sociedades Cooperativas de Producción que optan por diferir sus ingresos', + '621': 'Incorporación Fiscal', + '622': 'Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras', + '623': 'Opcional para Grupos de Sociedades', + '624': 'Coordinados', + '625': 'Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', + '626': 'Régimen Simplificado de Confianza', +}; + +const tipoRelacionLabels: Record = { + '01': 'Nota de crédito', + '02': 'Nota de débito', + '03': 'Devolución de mercancía', + '04': 'Sustitución de CFDI previo', + '05': 'Traslados de mercancias facturados previamente', + '06': 'Factura generada por traslados previos', + '07': 'CFDI por aplicación de anticipo', +}; + const usoCfdiLabels: Record = { G01: 'Adquisición de mercancías', G02: 'Devoluciones, descuentos o bonificaciones', @@ -142,13 +175,26 @@ export const CfdiInvoice = forwardRef( {/* Receptor */}
-
+

Receptor

{cfdi.nombreReceptor}

-

RFC: {cfdi.rfcReceptor}

+
+ RFC: {cfdi.rfcReceptor} + {cfdi.codigoPostalReceptor && ( + C.P.: {cfdi.codigoPostalReceptor} + )} + {cfdi.regimenFiscalReceptor && ( + + Régimen: {cfdi.regimenFiscalReceptor} + {regimenFiscalLabels[cfdi.regimenFiscalReceptor] + ? ` — ${regimenFiscalLabels[cfdi.regimenFiscalReceptor]}` + : ''} + + )} +
{cfdi.usoCfdi && ( -
+

Uso CFDI

{cfdi.usoCfdi} - {usoCfdiLabels[cfdi.usoCfdi] || ''} @@ -158,6 +204,41 @@ export const CfdiInvoice = forwardRef(

+ {/* CFDIs Relacionados */} + {(cfdi.cfdiTipoRelacion || cfdi.cfdisRelacionados) && ( +
+
+ + + +
+

CFDI Relacionado

+ {cfdi.cfdiTipoRelacion && ( +

+ Tipo de relación:{' '} + {cfdi.cfdiTipoRelacion} — {tipoRelacionLabels[cfdi.cfdiTipoRelacion] || 'Desconocido'} +

+ )} + {cfdi.cfdisRelacionados && ( +
+ UUIDs relacionados: +
+ {cfdi.cfdisRelacionados.split('|').map((uuid) => uuid.trim()).filter(Boolean).map((uuid, idx) => ( + + {uuid} + + ))} +
+
+ )} +
+
+
+ )} + {/* Datos del Comprobante */}
@@ -203,6 +284,7 @@ export const CfdiInvoice = forwardRef( Descripción + No. Id. Cant. P. Unitario Importe @@ -223,6 +305,9 @@ export const CfdiInvoice = forwardRef(

)} + + {concepto.noIdentificacion || } + {concepto.cantidad} {formatCurrency(concepto.valorUnitario)} diff --git a/apps/web/components/cfdi/cfdi-viewer-modal.tsx b/apps/web/components/cfdi/cfdi-viewer-modal.tsx index a729020..43ff033 100644 --- a/apps/web/components/cfdi/cfdi-viewer-modal.tsx +++ b/apps/web/components/cfdi/cfdi-viewer-modal.tsx @@ -14,6 +14,7 @@ interface CfdiConcepto { importe: number; claveProdServ?: string; claveUnidad?: string; + noIdentificacion?: string; } interface CfdiViewerModalProps { @@ -39,6 +40,7 @@ function parseConceptosFromXml(xmlString: string): CfdiConcepto[] { importe: parseFloat(el.getAttribute('Importe') || '0'), claveProdServ: el.getAttribute('ClaveProdServ') || undefined, claveUnidad: el.getAttribute('ClaveUnidad') || undefined, + noIdentificacion: el.getAttribute('NoIdentificacion') || undefined, }); } } @@ -75,6 +77,7 @@ export function CfdiViewerModal({ cfdi, open, onClose }: CfdiViewerModalProps) { importe: Number(c.importe), claveProdServ: c.claveProdServ || undefined, claveUnidad: c.claveUnidad || undefined, + noIdentificacion: c.noIdentificacion || undefined, }))); } else if (cfdi.xmlOriginal) { setConceptos(parseConceptosFromXml(cfdi.xmlOriginal)); diff --git a/docs/CAMBIOS-2026-05-09.md b/docs/CAMBIOS-2026-05-09.md index 44eeb54..b459f34 100644 --- a/docs/CAMBIOS-2026-05-09.md +++ b/docs/CAMBIOS-2026-05-09.md @@ -390,3 +390,42 @@ Se reemplazó por un estado de resultados vertical contable con 7 líneas, anál **Causa:** PostgreSQL devolvía `numeric` como string en el driver `pg`. Al sumar strings en el `reduce` del frontend, JavaScript concatenaba en lugar de sumar, generando `NaN` al formatear. **Fix:** Se agregó `::float` en las 5 queries SQL de CFDIs individuales del drill-down, forzando que el backend devuelva números reales. + +--- + +## 8. Visualizador de CFDI — campos faltantes + +**Fecha:** 2026-05-04 + +Se agregaron 5 campos adicionales al visualizador de CFDI (`CfdiInvoice`) para mostrar información completa del comprobante: + +| Campo | Origen | Backend | Frontend | +|---|---|---|---| +| **C.P. del receptor** | CFDI 4.0 `Receptor@DomicilioFiscalReceptor` | ✅ Migración + parser + sync + query | ✅ Tarjeta de receptor | +| **Régimen del receptor** | Ya existía en BD | — | ✅ Tarjeta de receptor con descripción | +| **No. identificación** (conceptos) | Ya existía en BD | — | ✅ Nueva columna en tabla de conceptos | +| **Tipo de relación** | Ya existía en BD | — | ✅ Sección "CFDI Relacionado" con descripción SAT | +| **CFDIs relacionados** (UUIDs) | Ya existía en BD | — | ✅ Badges con UUIDs separados por pipe | + +### Backend (`apps/api/`) + +| Archivo | Cambio | +|---|---| +| `src/migrations/tenant/044_cfdis_codigo_postal_receptor.sql` | Nueva migración: columna `codigo_postal_receptor VARCHAR(5)` + índice parcial | +| `src/services/sat/sat-parser.service.ts` | Extrae `codigoPostalReceptor` de `@_DomicilioFiscalReceptor` | +| `src/services/sat/sat.service.ts` | INSERT/UPDATE incluyen `codigo_postal_receptor` | +| `src/services/cfdi.service.ts` | `CFDI_SELECT` mapea `codigo_postal_receptor` → `"codigoPostalReceptor"` | +| `packages/shared/src/types/cfdi.ts` | Agregado `codigoPostalReceptor: string \| null` a interfaz `Cfdi` | + +### Frontend (`apps/web/`) + +| Archivo | Cambio | +|---|---| +| `components/cfdi/cfdi-invoice.tsx` | Renderizado de C.P., régimen, tipo relación, UUIDs relacionados, y columna "No. Id." en conceptos | +| `components/cfdi/cfdi-viewer-modal.tsx` | Mapea `noIdentificacion` desde DB y desde parseo XML | + +### Diccionarios agregados + +- **`regimenFiscalLabels`**: 20 regímenes fiscales (601–626) +- **`tipoRelacionLabels`**: 7 tipos de relación SAT (`01` Nota de crédito … `07` Aplicación de anticipo) +- **`usoCfdiLabels`**: ya existía, se reutiliza para el receptor diff --git a/packages/shared/src/types/cfdi.ts b/packages/shared/src/types/cfdi.ts index 6caa699..9e29672 100644 --- a/packages/shared/src/types/cfdi.ts +++ b/packages/shared/src/types/cfdi.ts @@ -92,6 +92,7 @@ export interface Cfdi { // Régimen fiscal regimenFiscalEmisor: string | null; regimenFiscalReceptor: string | null; + codigoPostalReceptor: string | null; // FK a tabla rfcs rfcEmisorId: number | null; rfcReceptorId: number | null;