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