Files
HoruxDespachosNuevo/docs/plans/2026-04-13-rol-admin-to-owner-rename.md

8.2 KiB

Rename del rol adminowner (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:

// 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

# 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 isGlobalAdminisGlobalOwner

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_RFCGLOBAL_OWNER_RFC

Mismo razonamiento. El RFC HTS240708LJA identifica al admin global de la plataforma; su nombre constante refleja ese concepto.

Renombrar bootstrap-horux360-admin.tsbootstrap-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.