# PROMPT SPRINT 1.3 — Schema del Catálogo

> **Para Ricci:** Pegá este prompt en una sesión NUEVA de Claude Code.
> Asegurate de estar en `/c/proyectos/innovium` y que Sprint 1.2 esté cerrado.

---

Hola Claude. Soy Ricci de Crono Systems. Sprint 1.2 cerrado, ahora vamos con **Sprint 1.3: Schema del catálogo de productos**.

## Estado actual

Sprint 1.2 cerrado con commits limpios. Lo confirmás:

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

Tenés:
- BD `innovium_master` con 5 tablas + 2 tenants activos (demo, infinia)
- BD `innovium_demo` con 8 tablas de sistema y datos
- BD `innovium_infinia` con 8 tablas de sistema y datos
- TenantResolver funcionando con subdominios `*.innovium.test`
- 34 tests verdes
- Login + dashboard styled funcionando para ambos tenants

## Lectura obligatoria antes de escribir código

En este orden:

1. `CLAUDE.md` — contexto del proyecto
2. `docs/ARQUITECTURA.md` — decisiones (especialmente ADR-001, ADR-002, ADR-009)
3. `docs/sprint-1-3/SCHEMA_CATALOGO.md` — las 8 tablas explicadas
4. `docs/sprint-1-3/CRITERIOS_ACEPTACION_1_3.md` — qué tiene que pasar para cerrar el sprint
5. `app/Models/User.php` — el patrón de data-mapper que ya implementamos en Sprint 1.1, vamos a replicarlo

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

## Alcance del sprint — solo schema y models

Este sprint es **schema-only + models**. NO hay UI nueva. Todo el trabajo es backend/datos.

### Lo que entra ✅

1. **8 migraciones tenant** en `database/migrations/tenant/` (numeradas 0009-0016 si las anteriores van de 0001-0008):
   - `0009_create_categorias_table.sql`
   - `0010_create_niveles_table.sql`
   - `0011_create_productos_table.sql`
   - `0012_create_producto_variantes_table.sql`
   - `0013_create_producto_imagenes_table.sql`
   - `0014_create_planes_lineas_table.sql`
   - `0015_create_precios_uf_table.sql`
   - `0016_create_tenant_config_table.sql`

2. **Aplicar las migraciones** a `innovium_demo` Y `innovium_infinia`. El comando `migrate:tenant <slug>` ya existe; usalo.

3. **8 Models** en `app/Models/`:
   - `Categoria.php`, `Nivel.php`, `Producto.php`, `ProductoVariante.php`, `ProductoImagen.php`, `PlanLinea.php`, `PrecioUf.php`, `TenantConfig.php`
   - **Patrón data-mapper** como `User.php` del Sprint 1.1 (FILLABLE whitelist, soft delete, getRoles equivalente, sin ORM)
   - **Sin lazy loading.** Relaciones son métodos explícitos con SQL visible.
   - **Models silenciosos.** No escriben audit_log (eso es responsabilidad del Service/Controller que llame).

4. **Seeder mínimo de `tenant_config`**: en cada tenant insertar 3 settings iniciales:
   - `valor_uf_actual` = `38500` (decimal)
   - `iva_porcentaje` = `19` (decimal)
   - `moneda_default` = `CLP` (string)

5. **Seeder inicial de `precios_uf`**: insertar 1 fila con la fecha de hoy y valor `38500`.

6. **Tests unitarios** en `tests/unit/`:
   - `CategoriaTest.php` — CRUD básico
   - `NivelTest.php` — CRUD básico
   - `ProductoTest.php` — CRUD + findByCategoria + findVendiblesParaPlan + soft delete
   - `PlanLineaTest.php` — crear plan con líneas mixtas (fijas + elección)
   - `TenantConfigTest.php` — get/set con tipos correctos
   - `PrecioUfTest.php` — getValorActual()

### Lo que NO entra ❌

- 🚫 Seed de productos genéricos chilenos → vacío, el admin los carga en Sprint 1.4
- 🚫 UI para gestionar productos / categorías / niveles → Sprint 1.4
- 🚫 Subida real de imágenes a R2 → Sprint 1.4
- 🚫 Visor 360° → Sprint 2.x
- 🚫 Integración API de UF → Sprint 2.x
- 🚫 Validación de stock real → Sprint 5.x+ (módulo bodega)

## Decisiones de diseño ya tomadas

### Patrón de Models

Replicar **exactamente** lo de `User.php`:
- Whitelist `FILLABLE` privada const
- `find($id)`, `all()` con filtro soft-delete
- `create($data)`, `update($id, $data)` validando contra FILLABLE
- `softDelete($id)` con `UTC_TIMESTAMP()` e idempotencia
- Métodos custom según el modelo (ej: `Producto::findVendiblesParaPlan()`)
- Sin escribir audit_log
- PDO prepared statements obligatorios
- `Database::current()` para obtener PDO del tenant

### Helper en `TenantConfig`

Implementar 2 métodos estáticos clave:

```php
TenantConfig::get(string $clave, mixed $default = null): mixed
// Lee el valor, parsea según el tipo guardado.
// Si la clave no existe, devuelve $default.

TenantConfig::set(string $clave, mixed $valor, ?int $userId = null): void
// Inserta o actualiza. Detecta el tipo del valor.
// Registra audit log con la clave (no el valor por si es sensible).
// Importante: TenantConfig::set SÍ escribe audit_log porque es una mutación 
// configurativa (no de catálogo).
```

### Helper en `PrecioUf`

```php
PrecioUf::getValorActual(): float
// Devuelve el valor más reciente con fecha <= hoy.
// Si no hay ninguno, fallback a TenantConfig::get('valor_uf_actual').
// Si tampoco, lanza excepción "valor UF no configurado".
```

### Tipos en `tenant_config`

El campo `valor` es siempre TEXT. La columna `tipo` indica cómo parsearlo:
- `string` → tal cual
- `int` → `(int)$valor`
- `decimal` → `(float)$valor`
- `boolean` → `$valor === '1'`
- `json` → `json_decode($valor, true)`
- `date` → `new DateTimeImmutable($valor)`

`TenantConfig::get()` hace este casting automáticamente.

### Orden de migraciones

Importante: **las migraciones tienen FKs**. El orden alfabético de los archivos garantiza que la tabla referenciada exista antes que la que la referencia. Verificar:

- `categorias` y `niveles` no tienen FK → primero
- `productos` referencia ambas → después
- `producto_variantes`, `producto_imagenes` referencian `productos` → después
- `planes_lineas` referencia `productos`, `categorias`, `niveles` → después
- `precios_uf` y `tenant_config` referencian `users` → al final

Las migraciones que numeré arriba (0009-0016) están en orden correcto.

### Sobre `productos.imagen_principal_id`

Hay un FK circular: `productos` → `producto_imagenes` → `productos`.

Resolverlo así:
1. Migración `0011_create_productos_table.sql`: NO incluir el FK a `producto_imagenes` (no existe todavía)
2. Migración `0013_create_producto_imagenes_table.sql`: crea la tabla con FK a `productos`
3. Migración `0017_alter_productos_add_imagen_fk.sql`: ALTER TABLE para agregar el FK posterior

O sea son **9 migraciones** en realidad, no 8. Está OK, el último ALTER es chico.

## Plan de commits sugerido

Atómicos:

1. `feat(db): migración tabla categorias`
2. `feat(db): migración tabla niveles`
3. `feat(db): migración tabla productos (sin FK a imagenes)`
4. `feat(db): migración tablas producto_variantes e imagenes`
5. `feat(db): migración tabla planes_lineas`
6. `feat(db): migración tablas precios_uf y tenant_config`
7. `feat(db): ALTER productos agregar FK a imagen_principal_id`
8. `feat(models): Categoria, Nivel, Producto, variantes, imagenes`
9. `feat(models): PlanLinea, PrecioUf, TenantConfig con helpers`
10. `feat(db): seeder inicial tenant_config y precios_uf`
11. `test: tests de models del catálogo`
12. `chore: aplicar migraciones a demo e infinia`
13. `chore: sprint 1.3 cerrado - schema catálogo`

## Cómo trabajar conmigo

1. Empezás leyendo todos los docs de la sección "lectura obligatoria"
2. **Confirmá 5 puntos antes de tocar archivos:**
   - ¿Leíste SCHEMA_CATALOGO.md y entendés las 8 tablas?
   - ¿Confirmas que los Models siguen el patrón de User.php (data-mapper, sin ORM, sin audit_log)?
   - ¿Confirmas que `tenant_config` ES la excepción (sus mutaciones SÍ escriben audit_log)?
   - ¿Tenés clara la solución del FK circular productos↔imagenes (ALTER posterior)?
   - ¿Tenés claro que NO sembramos productos genéricos? (las tablas quedan vacías)
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. Cuando termines, mostrame:
   - `git log --oneline | head -15`
   - `mysql -u root -e "SHOW TABLES" innovium_demo` (deben aparecer las 8 nuevas)
   - `mysql -u root -e "SHOW TABLES" innovium_infinia` (idem)
   - `mysql -u root -e "SELECT * FROM tenant_config" innovium_demo` (3 settings sembrados)
   - Output de `vendor/bin/phpunit` (todos verdes)

## Tiempo estimado

1-2 sesiones de Claude Code. El trabajo es schema + models + tests, sin UI ni integración compleja.

¡Vamos! 🚀
