# Setup admin global + gestión de clientes (2026-04-26 turno tardío) Sesión continuación del día. Foco: habilitar al admin global del fork para que pueda operar como plataforma — gestión de clientes, edición de catálogos (planes, add-ons), auto-facturación de pagos, y experiencia de login dedicada. > Relacionado: `2026-04-26-session.md` (índice del día), `2026-04-26-sprints-1-2-3.md` > (sprints anteriores), y los docs específicos de IVA y notificaciones. --- ## 1. Bootstrap admin global del fork El fork `Horux_despacho` no tenía tenant raíz ni `platform_admin`. Ejecutado: ```bash HORUX_ADMIN_EMAIL=carlos@horuxfin.com \ HORUX_ADMIN_NOMBRE=Carlos \ HORUX_TI_EMAIL=ivan@horuxfin.com \ HORUX_TI_NOMBRE=Ivan \ pnpm bootstrap:admin-global ``` Crea: - Tenant `Horux 360` (RFC `HTS240708LJA`, plan enterprise, dbMode MANAGED). - `carlos@horuxfin.com` con rol `platform_admin` (admin global). - `ivan@horuxfin.com` con rol `platform_ti` (TI superset). Contraseña fijada manualmente a `Admin12345!` con bcrypt cost 12 + `tokenVersion: increment` para invalidar JWTs viejos. **Cuentas finales del fork** (todas con `Admin12345!`): - `carlos@horuxfin.com` — platform_admin - `ivan@horuxfin.com` — platform_ti - `jd@demo.com` — owner Patito - `jf@demo.com` — owner Zorro - `supervisor@patito.com` / `auxiliar@patito.com` / `cliente@patito.com` --- ## 2. Login y home redirect `apps/web/app/(auth)/login/page.tsx` ahora detecta el admin global tras el login y lo manda directo a `/clientes` (gestión de tenants), no a `/dashboard` (operativo despacho). `dashboard/page.tsx` también tiene `useEffect` defensivo que redirige si llega ahí por URL directa. ```ts const isGlobalAdmin = isGlobalAdminRfc(response.user?.tenantRfc, userRole, platformRoles); if (isGlobalAdmin) router.push('/clientes'); ``` --- ## 3. Sidebar — banner "Configuración inicial" oculto `components/layouts/sidebar.tsx`: el banner que linkea a `/onboarding` solo se muestra cuando `(!contribuyentes || contribuyentes.length === 0) && role !== 'cliente' && !isGlobalAdmin`. El admin global nunca tendrá contribuyentes en su tenant raíz Horux 360 — el banner sería ruido permanente. (Una versión inicial también limpiaba todos los items operativos del sidebar para el admin global. Revertida — el admin global mantiene acceso completo para impersonar tenants. Solo cambia el banner.) --- ## 4. `/admin/usuarios` — fix `roleLabels[role].icon` undefined Bug latente desde que se introdujeron los roles del despacho. `roleLabels` solo tenía `owner/contador/visor` (Horux 360 mainline); cuando aparecía un user con rol `cfo`, `supervisor`, `auxiliar` o `cliente`, el lookup retornaba `undefined` y `roleInfo.icon` lanzaba TypeError. Fix: agregar los 7 roles + `defaultRoleInfo` como fallback defensivo: ```ts const roleLabels: Record = { owner: { label: 'Dueño', icon: Shield, color: 'text-primary' }, cfo: { label: 'CFO', icon: Briefcase, color: 'text-indigo-600' }, contador: { label: 'Contador', icon: Calculator, color: 'text-green-600' }, supervisor: { label: 'Supervisor', icon: UserCheck, color: 'text-blue-600' }, auxiliar: { label: 'Auxiliar', icon: UserCog, color: 'text-cyan-600' }, cliente: { label: 'Cliente', icon: User, color: 'text-amber-600' }, visor: { label: 'Visor', icon: Eye, color: 'text-muted-foreground' }, }; const defaultRoleInfo = { label: 'Sin rol', icon: User, color: 'text-muted-foreground' }; ``` --- ## 5. `/configuracion/precios-suscripcion` — planes despacho La página leía `plan_prices` (BD central, planes Horux 360 legacy: starter/business/business_ia/enterprise). En el fork no aplican. **Reescrita** para mostrar los 4 planes despacho desde `DESPACHO_PLAN_PRICES`: | Plan | Mensual | Anual 1er año | Anual renovación | RFCs | Timbres/mes | |---|---:|---:|---:|---:|---:| | Mi Empresa | $580 | $5,800 | $5,800 | 1 | 50 | | Mi Empresa + | $900 | $9,000 | $9,000 | 1 | 50 | | Business Control | No aplica | $25,850 | $25,850 | 100 | 0 | | Enterprise | No aplica | $43,000 | $43,000 | 100 | 0 | **Modo read-only** porque los precios viven en `DESPACHO_PLAN_PRICES` (catálogo estático en `@horux/shared`). ### Capacidad latente para edición Migración aplicada `20260426230000_despacho_plan_prices`: ```sql CREATE TABLE despacho_plan_prices ( plan TEXT PRIMARY KEY, monthly DECIMAL(10,2), first_year DECIMAL(10,2) NOT NULL, renewal DECIMAL(10,2) NOT NULL, permite_monthly BOOLEAN NOT NULL DEFAULT false, updated_at TIMESTAMP NOT NULL ); ``` Seed inicial con los 4 planes y sus valores. Permite migrar a editable cuando se quiera — service + endpoints + UI editable quedan como follow-up. --- ## 6. Gestión de clientes (`/clientes`) Página enriquecida con KPIs y operaciones para el admin global. ### Backend - **Service**: `apps/api/src/services/admin-clientes.service.ts` - `getClientesStats({ from, to })` — devuelve: - `suscripcionesPorPlan`: groupBy plan con count (status=authorized). - `ingresos`: sum + count de `Payment.amount` con status=approved en rango. - `noRenovaciones`: subs con `currentPeriodEnd` en rango y status terminal (cancelled / trial_expired / paused) + datos del tenant. - `usuariosPorCliente`: count de memberships activos por tenant. - `getTenantUsuarios(tenantId)` — drill-down: lista users + rol + isOwner. - **Controller**: `controllers/admin-clientes.controller.ts` con `requireStaff(req)` (platform_admin/ti). - **Routes**: `routes/admin-clientes.routes.ts` montadas en `/api/admin/clientes` (`GET /stats`, `GET /:tenantId/usuarios`). ### UI - Selector de rango de fechas (default mes en curso). - 4 KPI cards: clientes registrados / suscripciones activas (con breakdown por plan en pie) / ingresos del periodo / no renovaciones. - Tabla "Clientes que no renovaron" expandible con detalle (cliente, RFC, plan, vencimiento, status). - En cada item de la lista de clientes, click sobre el contador de usuarios abre un modal con la lista (email, nombre, rol, badge de owner). --- ## 7. Auto-facturación de pagos al cliente CLAUDE.md ya documentaba `invoicing.service.ts:emitInvoiceIfApplicable`. Cada webhook `payment.approved` lo dispara. Idempotente por `Payment.facturapiInvoiceId`, fail-soft si Facturapi falla. ### Ajustes en este turno - `PLAN_LABELS` extendido con los 4 planes despacho. Antes la factura mostraba el codename (`mi_empresa`, `business_cloud`) en lugar de "Mi Empresa" / "Enterprise". - Texto "Horux 360" → "Horux Despachos" en la descripción del CFDI y en el concepto de timbres. ### Prerequisito operativo (manual del admin) El emisor de las facturas es el tenant Horux 360 (RFC `HTS240708LJA`). Requiere: 1. **CSD válido** subido vía `/configuracion/csd` por Carlos (admin global). 2. `tenants.facturapi_org_id` poblado tras crear la organización en Facturapi. Hoy la columna está en `null` para el tenant recién bootstrapeado. Mientras falte, `emitInvoiceIfApplicable` registra error en consola y `Payment.facturapiInvoiceId` queda null. **Pendiente operativo**: que Carlos suba el CSD. --- ## 8. Gestión de add-ons (`/configuracion/addons`) Página nueva para que el admin global edite el catálogo de add-ons. ### Backend - **Controller**: `controllers/admin-addons.controller.ts` - `listCatalogo` — incluye count de suscripciones activas por add-on. - `updateCatalogoItem(id, { nombre?, precio?, active? })` con audit log. - **Routes**: `/api/admin/addons/catalogo` (`GET`, `PUT /:id`). `requireStaff`. ### UI - Tabla con columnas: codename, nombre (editable), precio MXN (editable), frecuencia, suscripciones activas (read-only), estado (badge), acciones. - Edición inline con dos botones (guardar / cancelar) por fila. - Toggle de activación con **confirmación condicional**: si el add-on tiene ≥1 suscripción activa, pide confirmar antes de desactivar. - Card link agregada al `/configuracion` raíz solo visible para admin global. ### Add-ons del catálogo actual - `lolita_ia_contribuyente` — Lolita IA (por contribuyente) — $250/mes - `modulo_ia` — Módulo IA Fiscal — $390/mes - `rfcs_extra_10` — +10 RFCs adicionales — $190/mes - `rfcs_extra_50` — +50 RFCs adicionales — $690/mes - `timbres_extra_500` — +500 timbres mensuales — $490/mes - `contribuyente_extra_business_cloud` — Contribuyente adicional — $45/mes (overage automático Business Control / Enterprise) --- ## 9. Sync al repo OneDrive Tras cada bloque de cambios, ejecutado: ```powershell robocopy "C:\Users\chtr1\Downloads\Horux_despacho" "C:\Users\chtr1\OneDrive\Documentos\GitHub\Horux_despachos" /E ` /XD node_modules .next .turbo dist .pnpm data email-previews .git xmls ` /XF .env .env.local "*.log" tsconfig.tsbuildinfo /NFL /NDL /NJH ``` Excluye `node_modules`, builds, caches, secretos, datos de tenants, y el `.git/` del destino para preservar el repo origen. --- ## 10. Pendientes derivados - **Cargar CSD del Horux 360** vía `/configuracion/csd` para activar auto-facturación real. Sin esto, `emitInvoiceIfApplicable` falla silencioso. - **Hacer editable `DespachoPlanPrice` desde UI**: la migración + seed están aplicados pero falta service + endpoint + UI editable. Mientras tanto la página de precios queda read-only. - **Notificación al desactivar add-on con suscripciones activas**: hoy se bloquean las contrataciones nuevas pero las activas siguen vigentes. Quizá enviar email al owner avisando que el add-on quedará "deprecated". - **Visual cue del prerequisito faltante de Facturapi org**: agregar alerta en `/clientes` o `/configuracion` cuando el tenant Horux 360 no tenga `facturapi_org_id` para que Carlos lo configure. --- ## 11. Pendientes históricos (sin cambio) - SMTP `.env` prod (cron de notificaciones loguea a consola sin SMTP). - Cloudflare Tunnel + `FRONTEND_URL` HTTPS dev. - Typecheck web cleanup (~18 errores preexistentes). - Nómina (tipo N) y Carta Porte sin implementar. - SAT rejections — esperando captura real. - Re-notificación bug 2.4 visibilidad auxiliares (sin reproducción específica).