490 lines
22 KiB
Markdown
490 lines
22 KiB
Markdown
# Resumen de cambios - 9 de mayo de 2026
|
||
|
||
## 1. Sincronización de pago - Alexa Torres
|
||
|
||
**Problema:** Alexa Torres (tenant `45ddd745-5037-4325-b3ec-1a85cbf7b849`) pagó $780 vía MercadoPago exitosamente, pero la suscripción seguía en estado `pending`. No llegó webhook.
|
||
|
||
**Causa raíz:**
|
||
- `.env` tenía `MP_ACCES_TOKEN` (1 S) en lugar de `MP_ACCESS_TOKEN` (2 S)
|
||
- La aplicación de MercadoPago tenía URL de webhook incorrecta (`https://www.horuxfin.com`) y sin tópicos suscritos
|
||
|
||
**Acciones:**
|
||
- Corregido typo en `.env`: `MP_ACCESS_TOKEN`
|
||
- Sincronizado manualmente el pago en BD:
|
||
- Creado registro `Payment` con `mpPaymentId = 158527899608`
|
||
- Actualizado suscripción a `status = authorized`
|
||
- Actualizado `currentPeriodEnd = 2026-06-09`
|
||
- Configurada URL de webhook en dashboard de MercadoPago: `https://horuxfin.com/api/webhooks/mercadopago`
|
||
- Seleccionados tópicos: `payment`, `subscription_preapproval`
|
||
|
||
**Estado:** ✅ Resuelto
|
||
|
||
---
|
||
|
||
## 2. Fix: Webhook MercadoPago - validación de firma
|
||
|
||
**Problema:** Error recurrente en logs:
|
||
```
|
||
TypeError: Cannot read properties of undefined (reading 'trim')
|
||
```
|
||
|
||
**Causa raíz:** `mercadopago.service.ts::verifyWebhookSignature` asumía que `x-signature` siempre tenía formato `key=value` bien formado.
|
||
|
||
**Fix:**
|
||
```ts
|
||
// Antes
|
||
const [key, value] = part.split('=');
|
||
parts[key.trim()] = value.trim();
|
||
|
||
// Después
|
||
const [key, value] = part.split('=');
|
||
if (!key || value === undefined) continue;
|
||
parts[key.trim()] = value.trim();
|
||
```
|
||
|
||
**Archivo:** `apps/api/src/services/payment/mercadopago.service.ts`
|
||
|
||
---
|
||
|
||
## 3. Notificación de primer pago pendiente de factura
|
||
|
||
**Problema:** Cuando un tenant realiza su primer pago, el sistema no factura automáticamente (por diseño), pero tampoco notifica al admin global.
|
||
|
||
### 3.1 Email al admin global
|
||
|
||
**Nuevos archivos:**
|
||
- `apps/api/src/services/email/templates/primer-pago-facturar.ts` — Template HTML del email
|
||
|
||
**Modificaciones:**
|
||
- `apps/api/src/services/email/email.service.ts` — Agregada función `sendPrimerPagoFacturar()`
|
||
- `apps/api/src/services/payment/invoicing.service.ts` — Cuando `emitInvoiceIfApplicable` detecta primer pago, envía email al admin
|
||
|
||
**Contenido del email:**
|
||
- Nombre, RFC del cliente
|
||
- Plan, monto, fecha de pago
|
||
- Botón directo a `/admin/facturas-pendientes`
|
||
|
||
### 3.2 Endpoints para admin global
|
||
|
||
**Nuevos endpoints en `apps/api/src/routes/facturacion.routes.ts`:**
|
||
- `GET /facturacion/pagos-sin-factura` — Lista payments `approved` sin `facturapiInvoiceId`
|
||
- `POST /facturacion/emitir-factura-pago/:paymentId` — Emite factura manual de un payment
|
||
|
||
**Nuevas funciones en `apps/api/src/controllers/facturacion.controller.ts`:**
|
||
- `getPagosSinFactura()` — Query con `hasPlatformRole('platform_admin')`
|
||
- `emitirFacturaPago()` — Emite factura usando datos fiscales del tenant pagador
|
||
|
||
**Exports agregados en `apps/api/src/services/payment/invoicing.service.ts`:**
|
||
- `getEmitterTenant()`
|
||
- `getCustomerFromTenant()`
|
||
- `buildInvoicePayload()`
|
||
|
||
### 3.3 Página de admin
|
||
|
||
**Nuevos archivos:**
|
||
- `apps/web/app/(dashboard)/admin/facturas-pendientes/page.tsx` — Tabla de pagos sin factura con botón "Emitir factura"
|
||
- `apps/web/lib/hooks/use-pagos-sin-factura.ts` — Hooks React Query
|
||
|
||
**Modificaciones:**
|
||
- `apps/web/lib/api/facturacion.ts` — Funciones `getPagosSinFactura()` y `emitirFacturaPago()`
|
||
- `apps/web/app/(dashboard)/clientes/page.tsx` — Métrica "Facturas pendientes" en KPIs
|
||
|
||
---
|
||
|
||
## 4. Fix: Vinculación de organización Facturapi - Horux 360
|
||
|
||
**Problema:** El tenant emisor Horux 360 (RFC `HTS240708LJA`) no tenía organización Facturapi vinculada. Al intentar emitir facturas daba:
|
||
```
|
||
Tenant emisor no tiene organización Facturapi
|
||
```
|
||
|
||
**Descubrimiento:** La BD del tenant (`horux_hts240708lja`) tenía una org incorrecta en `facturapi_orgs` (`69ff900f48058f06ef1234c0`) que no existía en Facturapi.
|
||
|
||
**Acciones:**
|
||
|
||
### BD Central
|
||
```sql
|
||
UPDATE tenants
|
||
SET facturapi_org_id = '69f23a5a242e0af47a41fa0d',
|
||
facturapi_org_key_enc = <encriptado>,
|
||
facturapi_org_key_iv = <encriptado>,
|
||
facturapi_org_key_tag = <encriptado>
|
||
WHERE rfc = 'HTS240708LJA';
|
||
```
|
||
|
||
### BD del tenant (`horux_hts240708lja`)
|
||
```sql
|
||
UPDATE facturapi_orgs
|
||
SET facturapi_org_id = '69f23a5a242e0af47a41fa0d',
|
||
api_key_enc = <encriptado>,
|
||
api_key_iv = <encriptado>,
|
||
api_key_tag = <encriptado>
|
||
WHERE contribuyente_id = '96f98a42-5f27-4f27-acf6-61822dea666c';
|
||
```
|
||
|
||
**API key generada:** `sk_live_bQC3XW7ZUVZxp9k9utN7DP6bRqehFZnZPtXhnDf1v1`
|
||
|
||
**Estado:** ✅ Resuelto
|
||
|
||
---
|
||
|
||
## 5. Fix: Autocompletado de RFCs y conceptos en facturación
|
||
|
||
**Problema:** Cuando un contribuyente estaba seleccionado en el dashboard, el autocompletado de RFCs y conceptos devolvía vacío si ese contribuyente no tenía CFDIs previos.
|
||
|
||
**Causa raíz:** Ambos endpoints filtraban por `contribuyente_id`, buscando solo en el historial del contribuyente activo.
|
||
|
||
**Fix aplicado:**
|
||
- `searchRfcs()` — eliminado filtro por `contribuyenteId`. Ahora busca en el catálogo completo de `rfcs`.
|
||
- `searchConceptos()` — eliminado filtro por `contribuyenteId`. Ahora busca conceptos en todos los CFDIs del tenant.
|
||
|
||
**Archivo:** `apps/api/src/controllers/facturacion.controller.ts`
|
||
|
||
---
|
||
|
||
## 6. Fix: CFDIs sin `contribuyente_id` en sincronizaciones SAT
|
||
|
||
**Problema:** Todos los CFDIs importados por SAT sync tenían `contribuyente_id = NULL`, aunque la columna ya existía. Esto causaba que no aparecieran facturas para conciliar ni en otros módulos que filtran por contribuyente.
|
||
|
||
**Causa raíz:** El cron job `sat-sync.job.ts` llamaba a `startSync(tenantId, syncType)` **sin pasar `contribuyenteId`**. Los jobs se creaban con `contribuyenteId = null`, y `saveCfdis()` insertaba los CFDIs con `contribuyente_id = null`.
|
||
|
||
**Fix aplicado:**
|
||
|
||
1. **`syncTenant()` (cron diario 3 AM)** — Ahora obtiene los contribuyentes del tenant desde su BD y ejecuta `startSync()` para cada uno pasando `contribuyenteId`. Si no hay contribuyentes, sincroniza a nivel tenant (legacy).
|
||
|
||
2. **`incrementalSyncTenant()` (cron incremental 11h/15h/19h)** — Mismo fix.
|
||
|
||
3. **`retryJob()` (reintento manual)** — Ahora pasa `job.contribuyenteId` al reintentar.
|
||
|
||
4. **Backfill de datos** — Se actualizaron los `contribuyente_id` de los CFDIs existentes para todos los tenants:
|
||
- Alexa Torres: 383 CFDIs
|
||
- Horux 360: 67 CFDIs
|
||
- Miguel Estrada: 84,429 CFDIs
|
||
- Aarón Ahumada: 2,290 CFDIs
|
||
- Humberto Torres: 33 CFDIs
|
||
|
||
**Archivos:**
|
||
- `apps/api/src/jobs/sat-sync.job.ts`
|
||
- `apps/api/src/services/sat/sat.service.ts`
|
||
|
||
---
|
||
|
||
## Archivos modificados
|
||
|
||
### Backend (`apps/api/`)
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `.env` | Fix typo `MP_ACCESS_TOKEN` |
|
||
| `src/services/payment/mercadopago.service.ts` | Fix validación firma webhook |
|
||
| `src/services/payment/invoicing.service.ts` | Notificación email + exports |
|
||
| `src/services/email/email.service.ts` | `sendPrimerPagoFacturar()` |
|
||
| `src/services/email/templates/primer-pago-facturar.ts` | **Nuevo** template |
|
||
| `src/controllers/facturacion.controller.ts` | `getPagosSinFactura()` + `emitirFacturaPago()` + fix `searchRfcs()` + fix `searchConceptos()` |
|
||
| `src/routes/facturacion.routes.ts` | Rutas `/pagos-sin-factura` + `/emitir-factura-pago/:paymentId` |
|
||
|
||
### Frontend (`apps/web/`)
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `lib/api/facturacion.ts` | `getPagosSinFactura()` + `emitirFacturaPago()` |
|
||
| `lib/hooks/use-pagos-sin-factura.ts` | **Nuevo** hooks |
|
||
| `app/(dashboard)/admin/facturas-pendientes/page.tsx` | **Nuevo** página admin |
|
||
| `app/(dashboard)/clientes/page.tsx` | KPI "Facturas pendientes" |
|
||
| `components/layouts/sidebar.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/sidebar-floating.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/sidebar-compact.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/topnav.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
|
||
---
|
||
|
||
## Configuración requerida en MercadoPago Dashboard
|
||
|
||
- **Aplicación:** Horux360 (ID: `5319386258998241`)
|
||
- **Webhook URL:** `https://horuxfin.com/api/webhooks/mercadopago`
|
||
- **Tópicos:** `payment`, `subscription_preapproval`
|
||
|
||
---
|
||
|
||
## Datos de organizaciones Facturapi
|
||
|
||
| Org | RFC | Uso |
|
||
|---|---|---|
|
||
| `69f23a5a242e0af47a41fa0d` | HTS240708LJA | Horux 360 (emisor principal) — ✅ Activa |
|
||
| `69ff900f48058f06ef1234c0` | — | Org fantasma (eliminada de BD) — ❌ Obsoleta |
|
||
| `69ff8fabc2053c5568d799c5` | XIA190128J61 | Org creada accidentalmente durante diagnóstico — ❌ Obsoleta |
|
||
|
||
---
|
||
|
||
## 7. Fix: Visor de CFDI en conciliación mostraba todo como "Cancelado" y faltaban datos
|
||
|
||
**Problema:** Al abrir cualquier CFDI desde el módulo de conciliación, el visor mostraba:
|
||
- Estatus: **CANCELADO** (aunque el CFDI estuviera vigente)
|
||
- Forma de pago: **-** (vacío)
|
||
- Serie/Folio: **S/N**
|
||
- Uso CFDI: no aparecía
|
||
- Totales desglosados (subtotal, descuento, impuestos): todos en 0
|
||
|
||
**Causa raíz:** El servicio `conciliacion.service.ts` solo seleccionaba un subconjunto mínimo de campos de la tabla `cfdis`. No incluía `status`, `forma_pago`, `serie`, `folio`, `uso_cfdi`, `subtotal`, `descuento`, `iva_traslado`, `iva_retencion`, `isr_retencion`, `moneda`, `tipo_cambio`, ni `fecha_cert_sat`.
|
||
|
||
Como `status` llegaba `undefined` al componente `CfdiInvoice`, la condición:
|
||
```tsx
|
||
cfdi.status === 'Vigente' || cfdi.status === '1' ? 'VIGENTE' : 'CANCELADO'
|
||
```
|
||
Siempre caía en el `else` mostrando CANCELADO.
|
||
|
||
**Fix aplicado en `apps/api/src/services/conciliacion.service.ts`:**
|
||
- Agregados todos los campos faltantes al `SELECT` SQL
|
||
- Agregados a la interfaz `ConciliacionCfdi`
|
||
- Agregados al mapeo de resultados con valores por defecto seguros (`|| 0`, `|| 'MXN'`, `|| 1`)
|
||
|
||
**Campos agregados:**
|
||
| Campo | Uso en visor |
|
||
|---|---|
|
||
| `status` | Badge VIGENTE / CANCELADO |
|
||
| `formaPago` | Datos del comprobante |
|
||
| `serie`, `folio` | Encabezado (serie-folio) |
|
||
| `usoCfdi` | Panel del receptor |
|
||
| `subtotal`, `descuento` | Totales |
|
||
| `ivaTraslado`, `ivaRetencion`, `isrRetencion` | Desglose de impuestos |
|
||
| `moneda`, `tipoCambio` | Moneda y tipo de cambio |
|
||
| `fechaCertSat` | Timbre fiscal digital |
|
||
|
||
---
|
||
|
||
## 8. Fix: Complementos de pago (tipo P) en conciliación usan fecha de emisión en lugar de fecha de pago
|
||
|
||
**Problema:** Los complementos de pago emitidos por Husberto en abril no aparecían en la conciliación de "Emitidas" de abril. Estaban en mayo. Ejemplo:
|
||
- Factura PPD: 2026-04-22 a TPA210222462 por $167,140.97
|
||
- Complemento de pago: emitido 2026-05-03, pero el pago fue el **2026-04-30**
|
||
|
||
El usuario esperaba verlo en abril porque el pago ocurrió en abril, pero el sistema filtraba por `fecha_emision` (mayo).
|
||
|
||
**Causa raíz:** El servicio de conciliación filtraba y ordenaba siempre por `fecha_emision`. Para los complementos de pago (tipo P), la fecha relevante es `fecha_pago_p` (fecha del pago documentado), no la fecha de emisión del CFDI.
|
||
|
||
**Fix aplicado:**
|
||
|
||
1. **Backend (`apps/api/src/services/conciliacion.service.ts`):**
|
||
- Filtros de fecha: `c.fecha_emision` → `COALESCE(c.fecha_pago_p, c.fecha_emision)`
|
||
- ORDER BY: `c.fecha_emision DESC` → `COALESCE(c.fecha_pago_p, c.fecha_emision) DESC`
|
||
- SELECT: agregado `c.fecha_pago_p as "fechaPagoP"`
|
||
- Interfaz y mapeo: agregado `fechaPagoP`
|
||
|
||
2. **Frontend (`apps/web/app/(dashboard)/conciliacion/page.tsx`):**
|
||
- Tabla "Por conciliar": `{new Date(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')}`
|
||
- Tabla "Conciliadas": mismo cambio
|
||
- Export Excel: mismo cambio
|
||
|
||
3. **Frontend (`apps/web/lib/api/conciliacion.ts`):**
|
||
- Interfaz `ConciliacionCfdi`: agregados todos los campos faltantes que ya existen en el backend (`serie`, `folio`, `fechaPagoP`, `subtotal`, `descuento`, `moneda`, `tipoCambio`, `formaPago`, `usoCfdi`, `status`, `fechaCertSat`, `ivaTraslado`, `ivaRetencion`, `isrRetencion`)
|
||
|
||
4. **Visor (`apps/web/components/cfdi/cfdi-invoice.tsx`):**
|
||
- Para tipo P con `fechaPagoP`, muestra "Pago: {fecha}" en lugar de la fecha de emisión
|
||
|
||
**Resultado:** Los complementos de pago ahora aparecen en el período donde ocurrió el pago real, no cuando se emitió el CFDI.
|
||
|
||
---
|
||
|
||
## Archivos modificados (actualizado)
|
||
|
||
### Backend (`apps/api/`)
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `.env` | Fix typo `MP_ACCESS_TOKEN` |
|
||
| `src/services/payment/mercadopago.service.ts` | Fix validación firma webhook |
|
||
| `src/services/payment/invoicing.service.ts` | Notificación email + exports |
|
||
| `src/services/email/email.service.ts` | `sendPrimerPagoFacturar()` |
|
||
| `src/services/email/templates/primer-pago-facturar.ts` | **Nuevo** template |
|
||
| `src/controllers/facturacion.controller.ts` | `getPagosSinFactura()` + `emitirFacturaPago()` + fix `searchRfcs()` + fix `searchConceptos()` |
|
||
| `src/routes/facturacion.routes.ts` | Rutas `/pagos-sin-factura` + `/emitir-factura-pago/:paymentId` |
|
||
| `src/jobs/sat-sync.job.ts` | Fix: pasa `contribuyenteId` en cron diario e incremental |
|
||
| `src/services/sat/sat.service.ts` | Fix: `retryJob()` pasa `contribuyenteId` + `saveCfdis()` usa `contribuyente_id` |
|
||
| `src/services/conciliacion.service.ts` | Fix: agrega campos faltantes (`status`, `formaPago`, impuestos, etc.) |
|
||
|
||
### Frontend (`apps/web/`)
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `lib/api/facturacion.ts` | `getPagosSinFactura()` + `emitirFacturaPago()` |
|
||
| `lib/hooks/use-pagos-sin-factura.ts` | **Nuevo** hooks |
|
||
| `app/(dashboard)/admin/facturas-pendientes/page.tsx` | **Nuevo** página admin |
|
||
| `app/(dashboard)/clientes/page.tsx` | KPI "Facturas pendientes" |
|
||
| `components/layouts/sidebar.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/sidebar-floating.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/sidebar-compact.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
| `components/layouts/topnav.tsx` | Removido "Facturas Pendientes" del menú admin |
|
||
|
||
---
|
||
|
||
## Configuración requerida en MercadoPago Dashboard
|
||
|
||
- **Aplicación:** Horux360 (ID: `5319386258998241`)
|
||
- **Webhook URL:** `https://horuxfin.com/api/webhooks/mercadopago`
|
||
- **Tópicos:** `payment`, `subscription_preapproval`
|
||
|
||
---
|
||
|
||
## Datos de organizaciones Facturapi
|
||
|
||
| Org | RFC | Uso |
|
||
|---|---|---|
|
||
| `69f23a5a242e0af47a41fa0d` | HTS240708LJA | Horux 360 (emisor principal) — ✅ Activa |
|
||
| `69ff900f48058f06ef1234c0` | — | Org fantasma (eliminada de BD) — ❌ Obsoleta |
|
||
| `69ff8fabc2053c5568d799c5` | XIA190128J61 | Org creada accidentalmente durante diagnóstico — ❌ Obsoleta |
|
||
|
||
---
|
||
|
||
## Notas técnicas
|
||
|
||
- La encriptación de API keys usa AES-256-GCM con clave derivada de `FIEL_ENCRYPTION_KEY` (SHA-256)
|
||
- El endpoint `POST /emitir-factura-pago/:paymentId` requiere rol `platform_admin`
|
||
- La regla "primer pago no se factura automáticamente" sigue vigente; los subsecuentes sí son automáticos
|
||
- Los CFDIs importados por SAT sync ahora se asocian correctamente al `contribuyente_id` correspondiente
|
||
|
||
---
|
||
|
||
## 12. Rediseño del Estado de Resultados en `/reportes`
|
||
|
||
**Fecha:** 4 de mayo de 2026
|
||
|
||
### Problema
|
||
El tab "Estado de Resultados" mostraba solo 4 KPI cards y dos listas con títulos engañosos (decían "Cliente/Proveedor" pero mostraban regímenes fiscales). No había análisis horizontal, vertical, ni drill-down.
|
||
|
||
### Solución
|
||
Se reemplazó por un estado de resultados vertical contable con 7 líneas, análisis comparativo vs año anterior, análisis vertical (% de ventas), drill-down por RFC → CFDI, exportación a Excel y filtro por régimen fiscal.
|
||
|
||
### Backend (`apps/api/`)
|
||
|
||
**Nuevos archivos/modificaciones:**
|
||
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `src/services/reportes.service.ts` | **Nuevas funciones:** `getEstadoResultadosDetallado`, `getEstadoResultadosDrillDown`, `exportEstadoResultadosToExcel` |
|
||
| `src/controllers/reportes.controller.ts` | **3 nuevos handlers:** `getEstadoResultadosDetallado`, `getEstadoResultadosDrillDown`, `exportEstadoResultados` |
|
||
| `src/routes/reportes.routes.ts` | Registradas 3 rutas nuevas |
|
||
| `packages/shared/src/types/reportes.ts` | **Nuevo tipo:** `EstadoResultadosDetallado` |
|
||
|
||
**Endpoints nuevos:**
|
||
- `GET /reportes/estado-resultados-detallado` — Tabla vertical con año anterior
|
||
- `GET /reportes/estado-resultados/drill-down?categoria=X&rfc=Y` — Resumen por RFC o CFDIs individuales
|
||
- `GET /reportes/estado-resultados/export` — Descarga Excel con formato condicional
|
||
|
||
**Lógica de cálculo:**
|
||
| Línea | Fórmula | Filtros |
|
||
|---|---|---|
|
||
| Ventas | `subtotal_mxn - descuento_mxn` | Emitidas tipo I, PUE/PPD, vigentes, excluyendo anticipos (`uso_cfdi != 'P01'` ni concepto `84111506`) |
|
||
| Devoluciones | `subtotal_mxn - descuento_mxn` | Emitidas tipo E, relación `01` o `03`, vigentes |
|
||
| Costo de ventas | `subtotal_mxn - descuento_mxn` | Recibidas tipo I, PUE/PPD, `uso_cfdi = 'G01'`, vigentes |
|
||
| Gastos operativos | `subtotal_mxn - descuento_mxn` (recibidos) + `total_mxn` (nómina) | Recibidas tipo I excluyendo G01 + Emitidas tipo N, vigentes |
|
||
| Totales | Calculados | Ventas netas, Utilidad bruta, Utilidad de la operación |
|
||
|
||
### Frontend (`apps/web/`)
|
||
|
||
| Archivo | Cambio |
|
||
|---|---|
|
||
| `app/(dashboard)/reportes/components/estado-resultados-table.tsx` | **Nuevo** — Tabla vertical con concepto, monto, % vertical, año anterior, variación % |
|
||
| `app/(dashboard)/reportes/components/drill-down-modal.tsx` | **Nuevo** — Modal de dos niveles: RFC resumen → CFDIs individuales |
|
||
| `lib/api/reportes.ts` | Agregados wrappers para los 3 endpoints nuevos |
|
||
| `lib/hooks/use-reportes.ts` | Agregados `useEstadoResultadosDetallado` y `useEstadoResultadosDrillDown` |
|
||
| `app/(dashboard)/reportes/page.tsx` | Integrada tabla nueva; conectado `RegimenSelector` al reporte; mantenidos Top 10 Clientes/Proveedores debajo |
|
||
|
||
### Fix posterior: Total NaN en drill-down nivel 2
|
||
|
||
**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
|
||
|
||
---
|
||
|
||
## 9. Fix: Facturas Facturapi no aparecen en complemento de pago
|
||
|
||
**Fecha:** 2026-05-20
|
||
|
||
**Problema:** Las facturas emitidas por Facturapi con método de pago PPD no aparecían en el dropdown de "complemento de pago" (tipo P). Solo aparecían las descargadas del SAT.
|
||
|
||
**Causa raíz:** Al emitir vía Facturapi, el campo `saldo_pendiente_mxn` quedaba `NULL`. El endpoint `GET /facturacion/cfdis-ppd` filtra con `COALESCE(saldo_pendiente_mxn, 0) > 0`, excluyendo las facturas de Facturapi.
|
||
|
||
**Fix:**
|
||
- Después del `INSERT` en `emitir()`, se llama `recomputarSaldoPendiente(pool, [uuid])` para facturas tipo I + método PPD.
|
||
- Backfill: se recalcularon 352 filas en la BD del tenant `horux_hts240708lja`.
|
||
|
||
**Archivos:**
|
||
- `apps/api/src/controllers/facturacion.controller.ts` — Agregado `recomputarSaldoPendiente` post-emisión
|
||
|
||
---
|
||
|
||
## 10. Seguridad: cancelación de facturas cruzada entre contribuyentes
|
||
|
||
**Fecha:** 2026-05-20
|
||
|
||
**Problema:** Un usuario viendo como contribuyente **Horux 360** podía cancelar facturas emitidas por **Consultoria Alcaraz Salazar**.
|
||
|
||
**Causa raíz:** El endpoint `POST /facturacion/cancelar/:uuid` no validaba ownership del contribuyente. Solo buscaba por UUID y cancelaba.
|
||
|
||
**Fix (backend):**
|
||
- El endpoint ahora recibe `contribuyenteId` del body.
|
||
- Si el caller envía un `contribuyenteId` y el CFDI pertenece a otro contribuyente → **403 Forbidden**.
|
||
|
||
**Fix (frontend):**
|
||
- `cancelarFactura` ahora pasa `selectedContribuyenteId` al backend.
|
||
- El botón de cancelar en la tabla de CFDIs solo se muestra si:
|
||
- Modo legacy: la factura no tiene `contribuyenteId`
|
||
- Modo multi-RFC: `cfdi.contribuyenteId === selectedContribuyenteId`
|
||
|
||
**Archivos:**
|
||
- `apps/api/src/controllers/facturacion.controller.ts` — Validación 403 + recepción de contribuyenteId
|
||
- `apps/web/lib/api/facturacion.ts` — `cancelarFactura` acepta `contribuyenteId`
|
||
- `apps/web/app/(dashboard)/cfdi/page.tsx` — Condicional de visibilidad del botón cancelar
|
||
|
||
---
|
||
|
||
## 11. Sync inicial SAT — Consultoria Alcaraz Salazar
|
||
|
||
**Fecha:** 2026-05-20
|
||
|
||
**Contexto:** La FIEL de Alcaraz Salazar se subió el 2026-05-19, pero la extracción de CSF falló por timeout del SAT. La sincronización inicial nunca se ejecutó (no había job `initial` en `sat_sync_jobs`).
|
||
|
||
**Acciones:**
|
||
- Creado job `initial` manualmente para contribuyente `bd9ba71c-55f9-40d5-a0d7-18909419298b`.
|
||
- El sync descubrió ~616 CFDIs en bloques 2024–2026.
|
||
- La tabla `rfcs` se pobló, habilitando el autocompletado del receptor en facturación.
|
||
|
||
**Estado:** ✅ Sync completado exitosamente
|
||
|