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

22 KiB
Raw Blame History

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

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_emisionCOALESCE(c.fecha_pago_p, c.fecha_emision)
    • ORDER BY: c.fecha_emision DESCCOALESCE(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.tscancelarFactura 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