Files
HoruxDespachosNuevo/docs/CAMBIOS-2026-05-09.md
Horux Dev e21ccd6860 fix(sat,conciliacion): propagar contribuyenteId en sync SAT y campos faltantes en visor de conciliacion
- sat-sync.job.ts: cron diario e incremental ahora iteran contribuyentes
  por tenant y pasan contribuyenteId a startSync(). Evita que CFDIs
  importados del SAT queden con contribuyente_id = NULL.

- sat.service.ts: retryJob() ahora reintenta con job.contribuyenteId.

- conciliacion.service.ts: agrega campos faltantes al SELECT de CFDIs:
  status, formaPago, serie, folio, usoCfdi, subtotal, descuento,
  moneda, tipoCambio, ivaTraslado, ivaRetencion, isrRetencion,
  fechaCertSat. Antes el visor mostraba 'CANCELADO' para todos los
  CFDIs (status era undefined) y faltaban datos de forma de pago,
  impuestos, serie/folio, etc.

Refs: docs/CAMBIOS-2026-05-09.md secciones 6 y 7
2026-05-11 03:58:53 +00:00

13 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:

  • .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:

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

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

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

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