Backend: - Migracion 044: codigo_postal_receptor VARCHAR(5) + indice - sat-parser: extrae DomicilioFiscalReceptor - sat.service: persiste codigo_postal_receptor en INSERT/UPDATE - cfdi.service: incluye codigo_postal_receptor en CFDI_SELECT - shared/types: codigoPostalReceptor en interfaz Cfdi Frontend: - cfdi-invoice: tarjeta receptor con C.P. y regimen (con descripciones) - cfdi-invoice: seccion CFDI Relacionado (tipo + UUIDs) - cfdi-invoice: columna No. Identificacion en tabla de conceptos - cfdi-viewer-modal: mapea noIdentificacion desde DB y XML
20 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
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 anteriorGET /reportes/estado-resultados/drill-down?categoria=X&rfc=Y— Resumen por RFC o CFDIs individualesGET /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 (601–626)tipoRelacionLabels: 7 tipos de relación SAT (01Nota de crédito …07Aplicación de anticipo)usoCfdiLabels: ya existía, se reutiliza para el receptor