- Agregar prop suggestions a FilterHeader con dropdown de opciones
- Calcular valores unicos de rfc/nombre emisor/receptor desde los
CFDIs cargados en memoria
- Filtrar sugerencias segun texto escrito (max 8 resultados)
- Al seleccionar una sugerencia se aplica el filtro y cierra el popover
- Mover FilterHeader fuera de ConciliacionPage para evitar
desmonte/remonte en cada render (causaba perdida de foco)
- Agregar debounce de 300ms al input de filtro para reducir
re-renders mientras el usuario escribe
- Quitado Invitaciones Trial del sidebar (4 layouts)
- Agregado tab Invitaciones Trial dentro de /admin/usuarios
- Componente reutilizable invitaciones-trial-tab.tsx
- Agregada nueva opcion Tareas en el sidebar principal
- Nueva pagina /tareas para ver y marcar tareas operativas
- Endpoint GET /tareas/mis-tareas con periodo actual
- Quitado boton de marcar completada de obligaciones fiscales en /pendientes
- Componente seguimiento-auxiliares.tsx con tabs Asignadas/Sin asignar
- Tabs internos Obligaciones/Tareas en cada vista
- API client y hooks para asignaciones
- Fix: invalidar query sin-asignar al asignar/desasignar
- Migracion 046: tablas obligacion_asignaciones y tarea_asignaciones
- Servicio y controller de asignaciones (CRUD + listados)
- Fix: enviar correo welcome al invitar usuario nuevo
- Fix: quitar JOIN users de queries tenant (usar Prisma en BD central)
- Fix: req.params.obligacionId correcto en asignaciones controller
- Fix: orden rutas estaticas antes de dinamicas en cartera.routes
- Fix: owner/cfo ven todas las asignaciones en getAsignacionesPorSupervisor
- Fix: validar que entidad pertenezca a cartera padre en subcartera
- Nuevo endpoint GET /carteras/asignaciones/sin-asignar
- Nuevo endpoint GET /tareas/mis-tareas
- Frontend: muestra input 'No. Cuenta Predial' en sección 'Datos del Inmueble'
cuando el régimen del emisor es 606 (Arrendamiento), antes de Conceptos
- Frontend: incluye cuentaPredial en payload; se resetea al cambiar contribuyente
- Backend: pasa property_tax_account a nivel de cada item en Facturapi
para facturapi.service.ts y contribuyente-facturapi.service.ts
- Build y deploy exitosos
- Cambia label de 'Precio Unitario (IVA incluido)' a 'Precio Unitario (sin IVA)'
- Elimina división interna price/(1+iva) en calcConcepto; ahora price es la base
- Cambia taxIncluded: true → false en payload enviado a backend
- Backend: tax_included default false en facturapi.service.ts y
contribuyente-facturapi.service.ts
- Build y deploy exitosos
- Frontend: input datetime-local visible solo para tipos I, E, T
(no P). Default al día actual a las 12:00. Se resetea al cambiar tipo.
- Frontend: validación en handleSubmit: fecha ≤ ahora y ≥ ahora-72h
- Backend controller: validación idéntica antes de consumir timbre
- Backend servicios: pasa campo 'date' al payload de Facturapi
cuando viene 'fechaEmision' en el body
- Build y deploy exitosos
Factura Global & fecha_efectiva:
- Migracion 045_factura_global.sql: periodicidad, meses_global, año_global, fecha_efectiva
- sat-parser.service.ts: extrae InformacionGlobal del XML
- sat.service.ts: calcFechaEfectiva con soporte bimestral (periodicidad 05)
- metricas-compute, dashboard, impuestos, cfdi, export, conciliacion, alertas:
reemplaza fecha_emision-1h por COALESCE(fecha_efectiva, fecha_emision-1h)
- Script recalc-metricas.ts para recalculo manual
Fallback datos fiscales tenant → contribuyente:
- contribuyente.service.ts: fetchTenantFiscalData + mergeContribuyenteWithTenant
rellena regimenFiscal, codigoPostal y domicilio cuando el contribuyente
tiene el mismo RFC que el tenant y sus campos estan vacios
- contribuyente.controller.ts y contribuyente-config.controller.ts:
pasan req.user!.tenantId al servicio
Fix critico SAT sync:
- sat.service.ts: anio_global → año_global en INSERT/UPDATE de CFDIs
(la migracion creo 'año_global' con tilde; el codigo usaba 'anio_global',
causando fallo en 100% de inserciones de CFDI)
- determineChunkMonths: salta sondeo si existe job previo con requestIds
- MAX_POLL_ATTEMPTS: 45 → 500 (~8h) para syncs iniciales grandes
Docs:
- docs/sessions/2026-05-22-factura-global-contribuyente-fallback.md
- Inicializar saldo_pendiente_mxn al emitir facturas I/PPD vía Facturapi
(antes quedaba NULL y no aparecían en complemento de pago)
- Validar ownership en cancelación: backend rechaza 403 si el caller
intenta cancelar una factura de otro contribuyente
- Frontend: ocultar botón cancelar si no se es el emisor de la factura
- Frontend: enviar contribuyenteId en la petición de cancelación
El controller ahora devuelve 504 (gateway timeout) con mensaje claro
en vez de 500 genérico cuando el scraper del SAT excede el tiempo.
Anteriormente solo capturaba errores con 'FIEL' en el mensaje;
los timeouts de page.waitForURL se escapaban como 500.
- createTenant ahora reusa User si el email ya existe globalmente
(hace upsert de membership en vez de crear user duplicado)
- Arregla error de express-rate-limit con X-Forwarded-For:
app.set('trust proxy', 1) para que funcione detrás de Cloudflare
- Tipos de email templates actualizados para tempPassword nullable
Para CFDIs tipo P (Pago) el visor ahora muestra:
- Monto pagado
- Fecha de pago
- Número de parcialidad
- UUID relacionado (factura pagada)
- Saldo insoluto
- Impuestos del pago (ISR/IVA/IEPS retenciones y traslados)
Además se ocultan para tipo P:
- Tabla de conceptos (dummy)
- Bloque de totales tradicional (subtotal/IVA/total)
- Sección CFDIs relacionados (reemplazada por UUID pagado)
El complemento de pago se renderiza en una card verde destacada.
El admin global ahora puede crear usuarios directamente desde
/admin/usuarios sin depender de que un owner los invite.
Backend:
- Nuevo endpoint POST /usuarios/global (controller + service)
- Valida límite de usuarios del plan del tenant destino
- Si el email ya existe, agrega membership al tenant destino
- Si no existe, crea user con temp password + membership
- Schema Zod: email, nombre, role, tenantId, supervisorUserId?
Frontend:
- Botón 'Agregar Usuario' en /admin/usuarios
- Formulario con: nombre, email, rol, empresa
- Hook useCreateUsuarioGlobal con invalidación de queries
Antes, createTenant() solo seteaba nombre, rfc, plan y databaseName.
Ahora registra tenants completos como despachos:
- dbMode: 'MANAGED'
- verticalProfile: CONTABLE | JURIDICO | ARQUITECTURA
- trialEndsAt: +30 días para plan trial
- codigoPostal: opcional (se llena automáticamente de la CSF al subir FIEL)
Frontend:
- Selector de Tipo de Despacho en /clientes
- C.P. omitido del formulario (viene de CSF -> sincronizarDatosFiscales)
- Tipos Tenant y CreateTenantData actualizados
Backend:
- getAllTenants y getTenantById retornan verticalProfile y codigoPostal
Refs: docs/sessions/2026-05-04-fix-clientes-crea-despacho.md
Problema: el boton 'Sincronizacion inicial (6 años)' desaparecia cuando
existia CUALQUIER job completado (daily, incremental, etc.). Esto era
inconsistente con el cron incremental del backend, que requiere
especificamente un job de tipo 'initial' completado.
Resultado: usuarios que solo habian hecho sync diaria perdian la opcion
de hacer la extraccion inicial completa, y el cron incremental tampoco
corria porque no habia initial.
Fix:
- Backend getSyncStatus: agrega lastCompletedInitialJob (busca solo
jobs type='initial' status='completed')
- Frontend SyncStatus: muestra el boton de inicial si
!lastCompletedInitialJob (ignora jobs diarios/incrementales)
- SatSyncStatusResponse: agrega campo lastCompletedInitialJob
Elimina la distincion entre despacho y no-despacho. Todos los tenants
ahora se comportan como despachos (multi-contribuyente, per-contribuyente
FIEL/CSD, roles de despacho, etc.).
Problema: isDespachoTenant(user?.tenantRfc) compara contra prefijo
'DESPACHO_' que ningun tenant real usa. Esto hacia que sat/page.tsx
siempre usara el endpoint legacy a nivel tenant, ignorando el contribuyente
seleccionado y mostrando datos del tenant en lugar del contribuyente.
Cambios:
- sat/page.tsx: elimina isDespachoTenant, usa selectedContribuyenteId
directamente para determinar contribId. Muestra banner cuando no hay
contribuyente seleccionado.
- csd/page.tsx: agrega banner de contribuyente seleccionado y oculta
la UI de CSD cuando no hay contribuyente seleccionado.
- tenant-selector.tsx: limpia selectedContribuyenteId al cambiar de
tenant para evitar stale state.
Script: apps/api/scripts/backfill-cp-receptor.ts
- Escanea 93,617 CFDIs con xml_original y codigo_postal_receptor IS NULL
- Extrae DomicilioFiscalReceptor via parseXml() (misma logica que sync SAT)
- Actualiza 53,858 registros en 6 tenants activos
- 0 fallos de parseo
- Backend (cfdi.service.ts): getConceptosList ahora soporta filtro noIdentificacion
via cc.no_identificacion ILIKE
- Frontend API (cfdi.ts): ConceptosFilters incluye noIdentificacion; se envía
como query param
- Frontend página (cfdi/page.tsx):
* Nuevo estado noIdentificacion en conceptosFilters
* Nueva columna 'No. Identificación' en header de tabla con Popover filtro
* Celda no_identificacion renderizada en cada fila
* Export a Excel respeta el nuevo filtro
- Nuevo endpoint GET /reportes/estado-resultados-detallado con cálculo contable:
* Ventas, Devoluciones, Ventas netas, Costo de ventas, Utilidad bruta,
Gastos operativos, Utilidad de la operación
* Fórmula: subtotal_mxn - descuento_mxn (sin impuestos), nómina usa total_mxn
* Excluye anticipos (uso_cfdi=P01 o clave_prod_serv=84111506)
* Filtro por régimen fiscal opcional
* Año anterior calculado automáticamente
- Nuevo endpoint GET /reportes/estado-resultados/drill-down:
* Nivel 1: resumen agrupado por RFC
* Nivel 2: CFDIs individuales filtrados por categoría
* Categorías: ventas, devoluciones, costo-ventas, gastos-operativos
- Nuevo endpoint GET /reportes/estado-resultados/export:
* Genera Excel con formato condicional (verde/rojo, negritas)
- Frontend:
* Tabla vertical con % vertical, año anterior y variación %
* Filas clickeables para drill-down modal de 2 niveles
* Top 10 Clientes/Proveedores mantenidos debajo
* Selector de régimen conectado al reporte
- Fix: NaN en total de drill-down nivel 2 por numeric como string en pg
* Agregado ::float en queries SQL de CFDIs individuales
Backend:
- client-invitations.service.ts: funcion resendInvitation() que
genera nuevo token, actualiza expiresAt y reenvia el email.
- Controller + routes: POST /invitations/client/:id/resend
Frontend:
- API client + hook useResendInvitation con invalidacion de cache.
- Pagina /admin/invitar-cliente: boton 'Reenviar' por cada
invitacion pendiente en la tabla.
Refs: docs/CAMBIOS-2026-05-09.md
El SelectValue del proyecto solo muestra el value raw (el ID).
Reemplazado por un span dentro de SelectTrigger que busca
el banco seleccionado por su ID y muestra el nombre + terminacion.
Creada funcion local formatCurrencyConciliacion con
minimumFractionDigits/maximumFractionDigits = 2.
El resto del sitio mantiene formatCurrency original sin decimales.
Backend:
- Nuevo modelo Prisma ClientInvitation con token unico, expiracion
y estados (pending/accepted/expired).
- Migracion: 20260511213955_add_client_invitations
- Service client-invitations.service.ts: crear invitacion,
validar token, registrar desde invitacion (reutiliza logica
de creacion de tenant + usuario de despacho.service).
- Controller + routes: POST /invitations/client (admin),
GET /invitations/client/validate/:token (publico),
POST /invitations/client/register/:token (publico),
GET /invitations/client (admin).
- Email template client-invitation.ts con link a
/invitacion/registro/{token}.
- Agregado sendClientInvitation a email.service.
Frontend:
- Pagina /invitacion/registro/[token] para que el invitado
complete registro (nombre, password, despacho, RFC, perfil).
- Pagina /admin/invitar-cliente para que admin global envie
invitaciones y vea el historial.
- Hooks useCreateInvitation, useValidateInvitationToken,
useRegisterFromInvitation, useClientInvitations.
- API client lib/api/client-invitations.ts.
Infra:
- PM2 ecosystem.config.js: usa node --import tsx con
kill_timeout aumentado a 15s para evitar EADDRINUSE.
- React Query retry=2 con delay exponencial para resiliencia.
Refs: docs/CAMBIOS-2026-05-09.md
- ecosystem.config.js: cambiado de tsx CLI a que es
mas estable. Aumentado kill_timeout de 5s a 15s para evitar
EADDRINUSE cuando PM2 reinicia el API.
- query-provider.tsx: agregado retry=2 con delay exponencial a las
queries de React Query para que sean mas resilientes a caidas
breves del API.
Refs: docs/CAMBIOS-2026-05-09.md
La condicion SQL NOT (metodo_pago = 'PPD' AND ...) producia NULL
cuando metodo_pago era NULL (como en complementos de pago tipo P),
lo que excluia silenciosamente todos los tipo P del listado de
emitidos en conciliacion.
Cambiada a: metodo_pago IS NULL OR metodo_pago != 'PPD' OR
regimen_fiscal_emisor IN ('605','616')
Esto mantiene la intencion original (excluir PPD de regimenes
que no son 605/616) sin afectar a tipo P ni otros sin metodo de pago.
Refs: docs/CAMBIOS-2026-05-09.md seccion 8
- 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
- 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
Backend:
- Notificación email al admin cuando llega primer pago aprobado (sin factura auto)
- Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global
- Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d)
- Fix webhook MP: validación defensiva de x-signature header
- Fix autocompleto RFCs: eliminado filtro por contribuyenteId
- Fix autocompleto conceptos: eliminado filtro por contribuyenteId
- SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds
- SAT sync request reuse across jobs para evitar agotar cuota diaria
- Typo fix MP_ACCESS_TOKEN en .env
- Trial invitations system backend
Frontend:
- Nueva página /admin/facturas-pendientes con tabla y emisión manual
- Métrica 'Facturas pendientes' en /clientes (clickable)
- Navegación onboarding FIEL/CSD corregida
- Sidebar themes sincronizados
- Fix SAT portal migration scraper (NetIQ)
- Trial invitation acceptance pages