Files
HoruxDespachosNuevo/docs/CAMBIOS-2026-05-09.md

490 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (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
---
## 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 20242026.
- La tabla `rfcs` se pobló, habilitando el autocompletado del receptor en facturación.
**Estado:** ✅ Sync completado exitosamente