- conciliacion.service.ts: filtros y ordenamiento ahora usan
COALESCE(fecha_pago_p, fecha_emision). Los CFDIs tipo P
(complementos de pago) aparecen en el periodo del pago real,
no de la emision del CFDI.
- conciliacion.service.ts: agrega fechaPagoP al SELECT y a la
interfaz ConciliacionCfdi.
- conciliacion/page.tsx: tablas y export Excel usan
fechaPagoP || fechaEmision para mostrar la fecha.
- cfdi-invoice.tsx: para tipo P con fechaPagoP, muestra
'Pago: {fecha}' en el encabezado.
- conciliacion.ts: actualiza interfaz ConciliacionCfdi con
todos los campos que ya devuelve el backend.
Refs: docs/CAMBIOS-2026-05-09.md secciones 7 y 8
15 KiB
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:
.envteníaMP_ACCES_TOKEN(1 S) en lugar deMP_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
PaymentconmpPaymentId = 158527899608 - Actualizado suscripción a
status = authorized - Actualizado
currentPeriodEnd = 2026-06-09
- Creado registro
- 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:
// 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ónsendPrimerPagoFacturar()apps/api/src/services/payment/invoicing.service.ts— CuandoemitInvoiceIfApplicabledetecta 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 paymentsapprovedsinfacturapiInvoiceIdPOST /facturacion/emitir-factura-pago/:paymentId— Emite factura manual de un payment
Nuevas funciones en apps/api/src/controllers/facturacion.controller.ts:
getPagosSinFactura()— Query conhasPlatformRole('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— FuncionesgetPagosSinFactura()yemitirFacturaPago()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
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)
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 porcontribuyenteId. Ahora busca en el catálogo completo derfcs.searchConceptos()— eliminado filtro porcontribuyenteId. 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:
-
syncTenant()(cron diario 3 AM) — Ahora obtiene los contribuyentes del tenant desde su BD y ejecutastartSync()para cada uno pasandocontribuyenteId. Si no hay contribuyentes, sincroniza a nivel tenant (legacy). -
incrementalSyncTenant()(cron incremental 11h/15h/19h) — Mismo fix. -
retryJob()(reintento manual) — Ahora pasajob.contribuyenteIdal reintentar. -
Backfill de datos — Se actualizaron los
contribuyente_idde 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.tsapps/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:
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
SELECTSQL - 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:
-
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
- Filtros de fecha:
-
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
- Tabla "Por conciliar":
-
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)
- Interfaz
-
Visor (
apps/web/components/cfdi/cfdi-invoice.tsx):- Para tipo P con
fechaPagoP, muestra "Pago: {fecha}" en lugar de la fecha de emisión
- Para tipo P con
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/:paymentIdrequiere rolplatform_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_idcorrespondiente