# PROMPT SPRINT 1.2 — Multi-tenancy core

> **Para Ricci:** Pegá este prompt en una sesión NUEVA de Claude Code, en `/c/proyectos/innovium`.
> Antes de pegar, asegurate de haber configurado el archivo `hosts` siguiendo `docs/sprint-1-2/CONFIGURAR_HOSTS_LOCAL.md` y de haber hecho `ping demo.innovium.test` correctamente respondiendo 127.0.0.1.

---

Hola Claude. Soy Ricci de Crono Systems. Sprint 1.1 está cerrado, ahora vamos con **Sprint 1.2: Multi-tenancy core**.

## Estado actual

Sprint 1.1 cerrado con 11 commits en `main` (local). Lo confirmás con:

```bash
git log --oneline | head -15
```

Tenés:
- BD `innovium_demo` con 8 tablas + migrations + seeds (6 usuarios, 6 roles, 25 permisos)
- Auth, Csrf, AuditLog funcionando
- 3 middlewares (Auth, Csrf, Rbac)
- Login + dashboard placeholder estilizados
- 29 tests pasando

## Lectura obligatoria antes de escribir código

En este orden:

1. `CLAUDE.md` — contexto del proyecto
2. `docs/ARQUITECTURA.md` — decisiones arquitectónicas (especialmente ADR-001 Modelo A multi-tenant, ADR-002 resolución por subdominio)
3. `docs/sprint-1-2/SCHEMA_MASTER.md` — las 5 tablas de master explicadas
4. `docs/sprint-1-2/CRITERIOS_ACEPTACION_1_2.md` — qué tiene que pasar para cerrar el sprint
5. `app/Core/Database.php` — ya tiene `tenant($slug)` y `server()`. **NO romper compatibilidad.**
6. `app/Core/Migrator.php` — ya funciona, vamos a reusarlo

**Hasta haber leído todo esto, NO empieces.**

## Alcance del sprint

### Lo que entra ✅

1. **Schema de `innovium_master`** — 5 tablas + migrations versionadas
2. **`app/Core/TenantResolver.php`** — clase que detecta subdominio del request → resuelve tenant
3. **Comando CLI `innovium tenant:create <slug>`** — crea tenant nuevo end-to-end
4. **Comando CLI `innovium master:migrate`** — aplica migraciones a `innovium_master`
5. **Seeds** para `tenants` y `tenant_features` (demo + infinia)
6. **Seeds** para tenant `infinia` (mismas 8 tablas que demo, datos sintéticos chilenos)
7. **Integración del TenantResolver en `public/index.php`** — antes del Router, resuelve tenant y lo inyecta en Container
8. **Cambio mínimo en views**: el tenant badge del login y dashboard debe leer del tenant resolved (no hardcoded "Demo")
9. **5 tests críticos de aislamiento** en `tests/integration/TenantIsolationTest.php`

### Lo que NO entra ❌

- 🚫 Selector visual de tenants para superadmin → Sprint 2.x
- 🚫 Crear/editar tenants desde UI → Sprint 2.x
- 🚫 Pantalla de "tenant suspendido" custom → 503 plain text está OK
- 🚫 Branding personalizado funcionando (color/logo custom por tenant) → Sprint 1.4
- 🚫 Migración real del flujo `tenant_admin_users` → Sprint 2.x (acá seedeamos directo en `users` del tenant)
- 🚫 Email confirmation en creación de admin → Sprint 2.x

## Decisiones arquitectónicas ya tomadas

### TenantResolver

- Lee `$_SERVER['HTTP_HOST']` al inicio del request
- Si host es `innovium.test` o `innovium.cl` (sin subdominio) → 404 con mensaje "tenant requerido"
- Si host es `admin.innovium.test` o `admin.innovium.cl` → modo superadmin (Sprint 2.x), por ahora 404 también
- Extrae el slug: `demo.innovium.test` → `demo`
- Valida regex `^[a-z][a-z0-9-]{2,30}$` antes de tocar BD
- Query a `innovium_master.tenants WHERE slug = ? AND eliminado_en IS NULL`
- Si no existe → 404 con mensaje genérico (no revelar si existe pero está suspendido)
- Si estado != `activo` y != `en_trial` → 503 "tenant temporalmente no disponible"
- Si todo OK → inyecta el `Tenant` (objeto inmutable) en el Container y abre conexión a `innovium_<slug>`
- Cachea el tenant resolved en el Container (no re-querya en el mismo request)

### Slug validation

```php
function isValidSlug(string $slug): bool {
    return (bool) preg_match('/^[a-z][a-z0-9-]{2,30}$/', $slug);
}
```

Reglas:
- Empieza con letra minúscula
- Solo letras minúsculas, dígitos y guión
- Largo entre 3 y 31 caracteres
- Reservados (NO permitir): `admin`, `api`, `www`, `app`, `mail`, `master`, `innovium`, `system`, `superadmin`, `cronosystems`

### Database conn pooling

- Cache por slug en `Database::tenant($slug)` (ya implementado en Sprint 1.1)
- En tests, asegurar reset de conexiones entre tests

### Tenant en Container

Crear `app/Core/Tenant.php` como **value object inmutable** con:
- `id`, `slug`, `nombre`, `nombre_corto`, `db_name`, `storage_path`, `estado`
- `getFeatures(): array` — carga `tenant_features` lazy en primer call
- `hasFeature(string $slug): bool`
- Inmutable: todos los properties readonly

Inyectar en Container con `$container->set('tenant', $tenant)`. Las clases que lo necesiten lo piden con `$container->get('tenant')`.

### Comando `tenant:create`

Sintaxis:
```bash
php scripts/innovium tenant:create <slug> --name="<nombre>" --short="<nombre_corto>" [--features=feature1,feature2]
```

Flujo:
1. Valida slug
2. Verifica que NO exista en `tenants`
3. CREATE DATABASE `innovium_<slug>`
4. Aplica todas las migrations a esa BD
5. Inserta fila en `innovium_master.tenants`
6. Inserta features default en `innovium_master.tenant_features`
7. (Opcional para Sprint 1.2) inserta admin inicial en `innovium_<slug>.users` con password aleatoria
8. Imprime resumen incluyendo la URL local: `http://<slug>.innovium.test:8000/login` y la password del admin
9. Si algo falla a mitad → rollback (drop database, eliminar fila de tenants si se insertó)
10. Registra en `tenant_audit_log` la acción

### Tests de aislamiento (5 mínimos)

1. **`testResolverDevuelveTenantCorrectoPorSubdominio`**: con host `demo.innovium.test`, resolver devuelve tenant con slug `demo`. Con `infinia.innovium.test`, devuelve `infinia`.

2. **`testUsuarioDeUnTenantNoVeDataDeOtro`**: login en demo como `vendedor@demo.cl`, query a `users` (vía model) → retorna solo usuarios de demo, NO ve `vendedor@infinia.cl`.

3. **`testHostnamesMaliciososSonRechazados`**: probá `'; DROP TABLE--.innovium.test`, hostname con `..`, slug vacío, slug con uppercase → todos rechazados con 404 antes de tocar BD.

4. **`testSlugCaseInsensitive`**: `Demo.innovium.test` (mayúscula) → resolver lowercase a `demo` y resuelve correctamente. O rechaza con 404 si decidimos ser estrictos. **Decidí vos** y documentá.

5. **`testTenantSuspendidoNoPuedeLoguearse`**: marcar tenant `demo` como `suspendido` → request a `demo.innovium.test/login` devuelve 503, no muestra el form de login.

## Plan de commits sugerido

Atómicos como en Sprint 1.1:

1. `feat(db): migraciones schema innovium_master (5 tablas)`
2. `feat(cli): comando master:migrate y bootstrap de master DB`
3. `feat(core): Tenant value object y TenantResolver`
4. `feat(http): integrar TenantResolver en index.php antes del Router`
5. `feat(cli): comando tenant:create con seed inicial`
6. `feat(db): seeds master (demo + infinia) y datos sintéticos infinia`
7. `style(views): tenant badge dinámico en login y dashboard`
8. `test: tests de aislamiento de tenants`
9. `chore: sprint 1.2 cerrado`

## Cómo trabajar conmigo

1. Empezás leyendo todo lo de "lectura obligatoria"
2. **Confirmá 5 puntos antes de tocar archivos:**
   - ¿Leíste SCHEMA_MASTER.md y entendés las 5 tablas?
   - ¿Confirmas que `innovium_master` y `innovium_<tenant>` son BDs separadas, no schemas?
   - ¿Confirmas validación de slug con regex y lista de reservados?
   - ¿Confirmas el flujo del comando `tenant:create` (rollback si falla)?
   - ¿Tenés clara la diferencia entre Sprint 1.2 (lo que hacemos ahora) y Sprint 2.x (selector visual, billing, email)?
3. Después de mi confirmación, arrancás con la primera migración
4. **Después de cada commit grande, mostrame qué hiciste y esperá mi OK antes de seguir**
5. Si tenés dudas técnicas, **preguntá** antes de inventar
6. Cuando termines todos los commits + tests, decímelo y yo levanto el server, hago `ping`, y valido visualmente con dos pestañas del navegador (demo + infinia) lado a lado

## Validación final del sprint

Cuando termines, voy a probar:

- [ ] `http://demo.innovium.test:8000/login` → tenant badge dice "Innovium · Demo"
- [ ] `http://infinia.innovium.test:8000/login` → tenant badge dice "Innovium · Infinia"
- [ ] Login con `vendedor@demo.cl` en demo → entra al dashboard de demo
- [ ] Login con `vendedor@infinia.cl` en infinia → entra al dashboard de infinia
- [ ] Login con `vendedor@demo.cl` en `infinia.innovium.test` → falla "Credenciales inválidas" (porque su user vive en demo)
- [ ] `http://innovium.test:8000/login` (sin subdominio) → 404
- [ ] `http://noexiste.innovium.test:8000/login` → 404
- [ ] `vendor/bin/phpunit` → 29 tests previos + nuevos de aislamiento, todos verdes

¡Vamos! 🚀
