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
This commit is contained in:
Horux Dev
2026-05-16 14:45:00 +00:00
parent 0bde43a309
commit bda0a4e212
8 changed files with 142 additions and 6 deletions

View File

@@ -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);

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -10,6 +10,7 @@ interface CfdiConcepto {
importe: number;
claveUnidad?: string;
claveProdServ?: string;
noIdentificacion?: string;
}
interface CfdiInvoiceProps {
@@ -66,6 +67,38 @@ const metodoPagoLabels: Record<string, string> = {
PPD: 'Pago en parcialidades o diferido',
};
const regimenFiscalLabels: Record<string, string> = {
'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<string, string> = {
'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<string, string> = {
G01: 'Adquisición de mercancías',
G02: 'Devoluciones, descuentos o bonificaciones',
@@ -142,13 +175,26 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
{/* Receptor */}
<div className="bg-gray-50 rounded-lg p-4 mb-5 border-l-4 border-blue-600">
<div className="flex items-start justify-between">
<div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Receptor</p>
<p className="text-lg font-semibold text-gray-800 mt-1">{cfdi.nombreReceptor}</p>
<p className="text-gray-600 text-sm">RFC: {cfdi.rfcReceptor}</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1 text-sm text-gray-600">
<span>RFC: {cfdi.rfcReceptor}</span>
{cfdi.codigoPostalReceptor && (
<span>C.P.: {cfdi.codigoPostalReceptor}</span>
)}
{cfdi.regimenFiscalReceptor && (
<span>
Régimen: {cfdi.regimenFiscalReceptor}
{regimenFiscalLabels[cfdi.regimenFiscalReceptor]
? `${regimenFiscalLabels[cfdi.regimenFiscalReceptor]}`
: ''}
</span>
)}
</div>
</div>
{cfdi.usoCfdi && (
<div className="text-right">
<div className="text-right ml-4 flex-shrink-0">
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Uso CFDI</p>
<p className="text-sm font-medium text-gray-700 mt-1">
{cfdi.usoCfdi} - {usoCfdiLabels[cfdi.usoCfdi] || ''}
@@ -158,6 +204,41 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
</div>
</div>
{/* CFDIs Relacionados */}
{(cfdi.cfdiTipoRelacion || cfdi.cfdisRelacionados) && (
<div className="bg-amber-50 rounded-lg p-3 mb-5 border border-amber-200">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wide">CFDI Relacionado</p>
{cfdi.cfdiTipoRelacion && (
<p className="text-sm text-gray-700 mt-1">
<span className="font-medium">Tipo de relación:</span>{' '}
{cfdi.cfdiTipoRelacion} {tipoRelacionLabels[cfdi.cfdiTipoRelacion] || 'Desconocido'}
</p>
)}
{cfdi.cfdisRelacionados && (
<div className="text-sm text-gray-700 mt-1">
<span className="font-medium">UUIDs relacionados:</span>
<div className="flex flex-wrap gap-2 mt-1">
{cfdi.cfdisRelacionados.split('|').map((uuid) => uuid.trim()).filter(Boolean).map((uuid, idx) => (
<span
key={idx}
className="inline-block px-2 py-0.5 bg-white border border-amber-200 rounded text-xs font-mono text-gray-600"
>
{uuid}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Datos del Comprobante */}
<div className="grid grid-cols-4 gap-3 mb-5">
<div className="bg-gray-50 rounded-lg p-3 text-center">
@@ -203,6 +284,7 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
<thead>
<tr className="bg-gray-100">
<th className="text-left py-3 px-4 font-semibold text-gray-700">Descripción</th>
<th className="text-center py-3 px-3 font-semibold text-gray-700 w-24">No. Id.</th>
<th className="text-center py-3 px-3 font-semibold text-gray-700 w-20">Cant.</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">P. Unitario</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">Importe</th>
@@ -223,6 +305,9 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
</p>
)}
</td>
<td className="text-center py-3 px-3 text-gray-700">
{concepto.noIdentificacion || <span className="text-gray-300"></span>}
</td>
<td className="text-center py-3 px-3 text-gray-700">{concepto.cantidad}</td>
<td className="text-right py-3 px-4 text-gray-700">
{formatCurrency(concepto.valorUnitario)}

View File

@@ -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));

View File

@@ -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 (601626)
- **`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

View File

@@ -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;