# Rename del rol `admin` → `owner` (label UI: "Dueño") ## Resumen Rename del rol per-tenant `admin` a `owner` en todo el código, con label visible en UI como **"Dueño"**. El concepto ortogonal de **"admin global"** (tenant RFC `HTS240708LJA` con acceso transversal a todos los tenants) se preservó sin cambios de nombre. ## Motivación Tener dos cosas llamadas "admin" generaba confusión recurrente: 1. **Admin del tenant** — rol local del tenant con permisos elevados (invitar usuarios, configurar, etc.) 2. **Admin global** — Horux 360 como dueño de la plataforma, con poder sobre todos los tenants En UI, mensajes, código y conversaciones era ambiguo: "Solo el admin puede..." ¿cuál admin? El rename disambigua sin perder el concepto de admin global. ## Cambios ### Código (identificadores) | Antes | Ahora | |-------|-------| | `Role = 'admin' \| 'cfo' \| ...` | `Role = 'owner' \| 'cfo' \| ...` | | `ROLES.admin` | `ROLES.owner` | | `role === 'admin'` | `role === 'owner'` | | `authorize('admin', 'cfo')` | `authorize('owner', 'cfo')` | | `where: { rol: { nombre: 'admin' } }` | `where: { rol: { nombre: 'owner' } }` | | `prisma.rol.findUnique({ where: { nombre: 'admin' } })` | `{ nombre: 'owner' }` | ### UI (labels visibles) | Antes | Ahora | |-------|-------| | "Administrador" | "Dueño" | | "Solo administradores pueden..." | "Solo los dueños pueden..." | | "Datos del Administrador" (registro) | "Datos del Dueño" | | "Administrador del Cliente" (crear tenant) | "Dueño del Cliente" | | "Nombre del Administrador" | "Nombre del Dueño" | | "Email del Administrador" | "Email del Dueño" | | "Contacta al administrador" (CSD sin timbres) | "Contacta al dueño de la cuenta" | | "Pide al administrador que configure MP" | "Pide al dueño de la cuenta..." | ### BD (`roles` table central) Row id=1: `nombre = 'admin'` → `nombre = 'owner'`. `descripcion = 'Administrador - acceso completo'` → `'Dueño - acceso completo'`. ### Demo user `admin@demo.com` (email preservado) ahora tiene rol `owner` en BD. Password sigue siendo `demo123`. El Dueño Demo es el label que verás en UI. ## Qué se preservó deliberadamente 1. **"Admin global" como concepto:** - Función `isGlobalAdmin(tenantId, role)` en `apps/api/src/utils/global-admin.ts` — sigue llamándose igual, internamente checa `role === 'owner'` - Función `isGlobalAdminRfc(tenantRfc, role)` en `packages/shared/src/constants/roles.ts` — idem - Constante `GLOBAL_ADMIN_RFC = 'HTS240708LJA'` — sin cambios - Mensajes de error como "Solo el administrador global puede..." quedan intactos 2. **Email `admin@demo.com`:** es una constante de test, el rename solo afecta el rol interno. El email sobrevive como identificador estable. 3. **Variables semánticas `adminEmail`, `adminNombre`** en payloads de `createTenant`: representan "el email/nombre del admin del cliente que estás creando". No son identificadores de rol. Podrían renombrarse en un pase separado si se desea, pero no afecta funcionalidad. 4. **Script `bootstrap-horux360-admin.ts`:** nombre del script preservado — "bootstrap del admin global" es el propósito, no el rol. Internamente crea un user con rol `owner`. ## Archivos tocados ### Shared - `packages/shared/src/types/auth.ts` - `packages/shared/src/constants/roles.ts` ### Backend API (17 archivos) - `prisma/seed.ts` — migración idempotente + role seed con `nombre: 'owner'` + demo user con `rolNombre: 'owner'` - `src/utils/global-admin.ts` - `src/services/auth.service.ts`, `tenants.service.ts`, `payment/subscription.service.ts`, `payment/mercadopago.service.ts` - `src/controllers/bancos.controller.ts`, `calendario.controller.ts`, `cfdi.controller.ts`, `conciliacion.controller.ts`, `facturacion.controller.ts`, `impuestos.controller.ts`, `regimen.controller.ts`, `usuarios.controller.ts` - `src/routes/documentos.routes.ts`, `facturacion.routes.ts`, `sat.routes.ts`, `subscription.routes.ts` ### Frontend Web (11 archivos) - `app/(auth)/register/page.tsx` - `app/(dashboard)/usuarios/page.tsx`, `admin/usuarios/page.tsx` - `app/(dashboard)/cfdi/page.tsx`, `calendario/page.tsx`, `documentos/page.tsx` - `app/(dashboard)/configuracion/page.tsx`, `configuracion/csd/page.tsx` - `app/(dashboard)/clientes/page.tsx` - `components/layouts/sidebar.tsx`, `sidebar-compact.tsx`, `sidebar-floating.tsx`, `topnav.tsx` ### Docs - `CLAUDE.md` — tabla de roles actualizada, explicación clara de "admin global" como concepto ortogonal - `README.md` — bullet nuevo en changelog v0.9.1 - `docs/plans/2026-04-13-subscriptions-self-serve.md` — referencias a admin pasan a owner donde corresponde ## Migración idempotente El seed incluye este SQL al inicio de la sección de roles: ```typescript // Migración: renombra el rol legacy 'admin' a 'owner' si sobrevive de un seed viejo. // Idempotente (no-op si ya se renombró o nunca existió). await prisma.$executeRawUnsafe(`UPDATE roles SET nombre = 'owner' WHERE nombre = 'admin'`); ``` Esto permite que: - Desarrolladores con BD vieja solo corran `pnpm db:seed` una vez para converger - Deploys a prod apliquen el rename sin necesidad de migración SQL manual - Re-seedear no rompe la BD actualizada ## Impacto en runtime ### JWT invalidados Los tokens JWT emitidos **antes del rename** tienen `role: 'admin'`. Después del rename, el middleware de auth usa Zod para validar el payload contra el nuevo enum `Role`, que ya no acepta `'admin'`. Resultado: cualquier sesión activa obtiene 401 en la siguiente request → se fuerza re-login. **Mitigación:** anuncio en release notes y email a clientes si se despliega a producción. En dev, simplemente cerrar sesión y volver a entrar. ### Queries SQL directas Si hay scripts externos al repo (reportes, jobs de ETL, etc.) que filtran por `role = 'admin'` en la tabla `users` o joins, **fallarán silenciosamente** (filtro no matchea nada). Revisar integraciones externas antes de desplegar a prod. ### API consumidores externos Si el API tiene consumidores que envían `role: 'admin'` en payloads de creación/update de usuarios, esos requests fallarán la validación de Zod. Actualizar docs de API + comunicar a integradores. ## Verificación ```bash # Backend cd apps/api pnpm typecheck # 0 errores # BD pnpm db:seed # idempotente — renombra legacy admin → owner, crea rol 'owner' si falta ``` Al correr seed post-rename, deberías ver en log: ``` ✅ 5 roles cargados ✅ User created: admin@demo.com (owner) ``` ### Tests manuales 1. Login con `admin@demo.com / demo123` → JWT nuevo trae `role: 'owner'` 2. Página `/usuarios` muestra "Dueño" como role label para el usuario demo 3. Select al invitar nuevo usuario: opción "Dueño" (antes "Administrador") 4. Mensajes de error tipo "Solo los dueños pueden invitar usuarios" cuando un contador intenta 5. **"Admin global" preservado**: ingresando como `admin@horux360.com` (si corriste `bootstrap:admin-global`) se ve la sección Clientes + Admin Usuarios + Todas las Suscripciones — comportamiento idéntico al de antes del rename ## Decisiones descartadas ### Renombrar `isGlobalAdmin` → `isGlobalOwner` **Tentación:** consistencia léxica. **Por qué no:** el concepto a nivel producto sigue llamándose "admin global" — así lo dice la UI, los docs, y así lo entienden los usuarios. Renombrar solo la función interna crea disonancia entre código y lenguaje del producto. 8 callsites tocados sin ganancia funcional. ### Renombrar `GLOBAL_ADMIN_RFC` → `GLOBAL_OWNER_RFC` Mismo razonamiento. El RFC `HTS240708LJA` identifica al admin global de la plataforma; su nombre constante refleja ese concepto. ### Renombrar `bootstrap-horux360-admin.ts` → `bootstrap-horux360-owner.ts` El script provisiona el tenant del admin global. Renombrarlo confunde el propósito (alguien pensaría que bootstrappea cualquier owner). ## Pendientes opcionales 1. **Variables `adminEmail` / `adminNombre` en tipos de API:** renombrarlas a `ownerEmail` / `ownerNombre` por consistencia total. Requiere actualizar integraciones externas. No se hizo porque no es bloqueante. 2. **Revisar integraciones externas/scripts** que asuman `role = 'admin'` — fuera del alcance de este rename (no viven en este repo). 3. **Tests automatizados de authorization:** confirmarían que el rename no rompió ningún endpoint. Actualmente solo hay verificación manual + typecheck.