# 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 = , facturapi_org_key_iv = , facturapi_org_key_tag = WHERE rfc = 'HTS240708LJA'; ``` ### BD del tenant (`horux_hts240708lja`) ```sql UPDATE facturapi_orgs SET facturapi_org_id = '69f23a5a242e0af47a41fa0d', api_key_enc = , api_key_iv = , api_key_tag = 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