# Sprint 1.3 · Schema del Catálogo

> **Para vos, Ricci.** Las 8 tablas del catálogo de productos explicadas en castellano.

## Filosofía

Este sprint es **schema only** — creamos las tablas, comandos CLI, models y validaciones. **No hay UI nueva.** Las pantallas de admin para cargar productos se construyen en Sprint 1.4.

**Las tablas quedan VACÍAS** al terminar este sprint. Cada admin de funeraria carga sus propios productos.

---

## Las 8 tablas (todas viven en `innovium_<tenant>`)

### 1. `categorias` — agrupaciones temáticas

> Cada producto pertenece a una categoría: Urnas, Relicarios, Ánforas, Servicios, etc.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | id |
| `slug` | VARCHAR(60) UNIQUE | identificador URL: `urnas`, `relicarios` |
| `nombre` | VARCHAR(100) | "Urnas", "Relicarios" |
| `descripcion` | TEXT NULL | descripción larga |
| `icono` | VARCHAR(50) NULL | nombre del icono Lucide para UI: `package`, `flame`, etc. |
| `orden_visual` | INT | para ordenar en pantalla |
| `tipo` | ENUM | `producto`, `servicio` (servicios incluyen capilla, traslados, etc.) |
| `activo` | TINYINT(1) | |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |
| `eliminado_en` | DATETIME NULL | soft delete |

**Decisión:** `tipo` permite distinguir productos físicos de servicios sin necesidad de tabla separada. Un servicio (capilla velatoria) y un producto (urna) viven en la misma tabla `productos` pero con su categoría correspondiente.

---

### 2. `niveles` — gama/clase del producto

> Tu requerimiento: cada producto tiene un nivel (austero, tradicional, premium).
> El nombre lo decide cada funeraria.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `slug` | VARCHAR(50) UNIQUE | `austero`, `tradicional`, `premium` |
| `nombre` | VARCHAR(100) | "Austero", "Tradicional Plus", etc. |
| `descripcion` | TEXT NULL | |
| `orden_visual` | INT | 1=más bajo, 99=más alto |
| `color_hex` | VARCHAR(7) NULL | para chips de color en UI: `#22D3EE`, etc. |
| `activo` | TINYINT(1) | |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |
| `eliminado_en` | DATETIME NULL | |

**Ejemplos posibles:**
- Funeraria A: Austero (1) / Tradicional (2) / Premium (3)
- Funeraria B: Clase C (1) / Clase B (2) / Clase A (3)
- Funeraria C: Económico (1) / Estándar (2) / Plus (3) / Élite (4)

**Cada tenant define los suyos.** Sin restricción de cantidad.

---

### 3. `productos` — catálogo principal

> El corazón del módulo. Acá viven urnas, ataúdes, relicarios, ánforas, capilla velatoria, traslados, exhumaciones, etc. **Y también los planes** (con `tipo='plan'`).

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `codigo` | VARCHAR(50) UNIQUE | código interno: `URN-OLM-001` |
| `nombre` | VARCHAR(200) | "Urna de Olmo Tradicional" |
| `descripcion_corta` | VARCHAR(500) NULL | resumen para tarjetas |
| `descripcion_larga` | TEXT NULL | detalle para vista de producto |
| `tipo` | ENUM | `producto`, `servicio`, `plan` |
| `categoria_id` | BIGINT FK | |
| `nivel_id` | BIGINT FK NULL | NULL para servicios y planes |
| `precio_base_clp` | DECIMAL(12,2) | precio NI en CLP |
| `precio_base_uf` | DECIMAL(8,2) NULL | precio NF en UF (si aplica) |
| `disponible_para` | ENUM | `NI`, `NF`, `AMBOS` |
| `vendible_como_componente` | TINYINT(1) | puede entrar en un plan |
| `vendible_como_adicional` | TINYINT(1) | puede venderse suelto en contrato |
| `requiere_stock` | TINYINT(1) | controla bodega (default false en este sprint) |
| `stock_actual` | INT NULL | placeholder para módulo bodega |
| `imagen_principal_id` | BIGINT FK NULL | FK a `producto_imagenes` |
| `activo` | TINYINT(1) | |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |
| `eliminado_en` | DATETIME NULL | |

**Decisiones clave:**

- **`tipo='plan'`** identifica registros que son planes (NI o NF). Sus componentes están en `planes_lineas`.
- **Precio puede estar en CLP, UF o ambos.** `disponible_para` marca para qué tipo de contrato se ofrece.
- **`vendible_como_*`** son flags independientes: un producto puede ser componente de plan Y vendible como adicional (caso típico de los relicarios).
- **`stock_actual`** queda como placeholder; el control real de bodega es Sprint 5.x+.

---

### 4. `producto_variantes` — variantes mismo precio

> "Urna Olmo medida especial" es la misma urna olmo, mismo precio, distinta medida.
> No queremos duplicar el producto, solo registrar la variante.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `producto_id` | BIGINT FK | |
| `nombre_variante` | VARCHAR(150) | "Medida especial", "XL", "Color claro" |
| `sku` | VARCHAR(50) NULL | código de inventario opcional |
| `ajuste_precio_clp` | DECIMAL(10,2) | 0 si igual al padre, +/- si difiere |
| `orden_visual` | INT | |
| `activo` | TINYINT(1) | |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |

**Confirmación:** `ajuste_precio_clp` por default 0 (mismo precio que el padre). Si en el futuro una variante necesita precio distinto, se setea ahí.

---

### 5. `producto_imagenes` — galería de fotos

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `producto_id` | BIGINT FK | |
| `r2_path` | VARCHAR(500) | path en R2: `tenants/infinia/productos/urn-001/01.jpg` |
| `orden` | INT | 1=primera foto, 2=segunda, etc. |
| `descripcion` | VARCHAR(200) NULL | texto alt para accesibilidad |
| `tipo` | ENUM | `principal`, `galeria`, `360_frame` |
| `creado_en` | DATETIME | |

**Decisiones:**
- **Sprint 1.3 solo soporta galería estática** (`tipo='galeria'` y `'principal'`).
- **`tipo='360_frame'`** queda preparado para Sprint 2.x cuando agreguemos visor 360°. Cada frame del 360° es una imagen con orden 1-36.

---

### 6. `planes_lineas` — composición de un plan

> Acá vive la lógica que conversamos: planes con líneas fijas, líneas a elección de categoría, líneas opcionales.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `plan_id` | BIGINT FK | FK a `productos` donde tipo='plan' |
| `modo` | ENUM | `producto_fijo`, `eleccion_categoria`, `eleccion_nivel` |
| `producto_id` | BIGINT FK NULL | si modo=`producto_fijo` |
| `categoria_id` | BIGINT FK NULL | si modo=`eleccion_categoria` |
| `nivel_id` | BIGINT FK NULL | filtro adicional para `eleccion_categoria` |
| `cantidad` | INT | cantidad incluida |
| `obligatorio` | TINYINT(1) | si false, el cliente puede declinar |
| `orden_visual` | INT | |
| `notas` | TEXT NULL | texto descriptivo opcional |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |

**Ejemplos de uso (cuando se cargue data):**

```sql
-- Línea fija: 1 Urna Olmo Tradicional específica
modo='producto_fijo', producto_id=123, cantidad=1, obligatorio=true

-- Línea elección: 3 relicarios a elección de cualquier categoría Relicarios
modo='eleccion_categoria', categoria_id=5, cantidad=3, obligatorio=true

-- Línea elección con nivel: 2 ánforas a elección, pero solo nivel premium
modo='eleccion_categoria', categoria_id=8, nivel_id=3, cantidad=2, obligatorio=false
```

**Decisión clave:** este modelo permite TODOS los casos que mencionaste sin requerir refactor futuro.

---

### 7. `precios_uf` — cache local del valor UF

> Cuando llegue contratos en UF, necesitamos saber el valor del día.
> Esta tabla guarda un cache local que el admin actualiza manualmente.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `fecha` | DATE UNIQUE | fecha del valor (uno por día) |
| `valor_clp` | DECIMAL(10,2) | UF en CLP ese día |
| `fuente` | VARCHAR(100) | `manual`, `api_sii`, `api_bcch`, etc. |
| `actualizado_por` | BIGINT FK | user_id del admin que lo cargó |
| `creado_en` | DATETIME | |

**Sprint 1.3:** se siembra UN valor inicial (la fecha de hoy con un UF aprox de $38.500).
**Sprint 1.4:** UI para que el admin actualice diariamente.
**Sprint 2.x:** integración con API real (BCCh / mindicador.cl) y cron diario.

---

### 8. `tenant_config` — configuración del tenant

> Settings clave-valor para que el admin pueda ajustar cosas del tenant sin tocar código.

| Columna | Tipo | Para qué |
|---|---|---|
| `id` | BIGINT auto | |
| `clave` | VARCHAR(100) UNIQUE | `valor_uf_actual`, `iva_porcentaje`, `dias_pago_default` |
| `valor` | TEXT | el valor (siempre como string, se parsea según `tipo`) |
| `tipo` | ENUM | `string`, `int`, `decimal`, `boolean`, `json`, `date` |
| `categoria` | VARCHAR(50) | agrupación: `monetario`, `legal`, `operativo`, `branding` |
| `descripcion` | TEXT NULL | qué hace este setting |
| `editable_por_admin` | TINYINT(1) | si el admin puede modificarlo, o solo Crono Systems |
| `actualizado_por` | BIGINT FK NULL | user_id |
| `creado_en` | DATETIME | |
| `actualizado_en` | DATETIME | |

**Settings iniciales sembrados (mínimos):**
- `valor_uf_actual` = `38500` (string, tipo=decimal, categoria=monetario)
- `iva_porcentaje` = `19` (string, tipo=decimal, categoria=monetario)
- `moneda_default` = `CLP` (string, tipo=string, categoria=monetario)

**Decisiones clave:**
- **Patrón clave-valor flexible:** agregar nuevos settings no requiere migración de schema.
- **`editable_por_admin`:** algunos settings son críticos y solo Crono Systems los toca (ej: `feature_flags_activos`).
- **`actualizado_por`** + audit log: trazabilidad de cambios.

---

## Foreign Keys importantes

```sql
productos.categoria_id → categorias.id ON DELETE RESTRICT
productos.nivel_id → niveles.id ON DELETE RESTRICT
productos.imagen_principal_id → producto_imagenes.id ON DELETE SET NULL

producto_variantes.producto_id → productos.id ON DELETE CASCADE
producto_imagenes.producto_id → productos.id ON DELETE CASCADE

planes_lineas.plan_id → productos.id ON DELETE CASCADE
planes_lineas.producto_id → productos.id ON DELETE RESTRICT
planes_lineas.categoria_id → categorias.id ON DELETE RESTRICT
planes_lineas.nivel_id → niveles.id ON DELETE RESTRICT

precios_uf.actualizado_por → users.id ON DELETE SET NULL
tenant_config.actualizado_por → users.id ON DELETE SET NULL
```

**Lógica:** cuando borrás un producto físicamente, sus variantes/imágenes deben morir con él (CASCADE), pero las categorías/niveles a las que pertenecía deben preservarse (RESTRICT).

---

## Índices críticos

```sql
KEY idx_productos_tipo (tipo)
KEY idx_productos_categoria (categoria_id)
KEY idx_productos_nivel (nivel_id)
KEY idx_productos_disponible (disponible_para, activo)
KEY idx_productos_vendible_componente (vendible_como_componente, activo)
KEY idx_productos_vendible_adicional (vendible_como_adicional, activo)

KEY idx_planes_lineas_plan (plan_id, orden_visual)
KEY idx_planes_lineas_modo (modo)

UNIQUE KEY uq_precios_uf_fecha (fecha)
KEY idx_precios_uf_fecha_desc (fecha DESC)  -- para "valor más reciente"

UNIQUE KEY uq_tenant_config_clave (clave)
KEY idx_tenant_config_categoria (categoria)
```

---

## Models que se construyen

```
app/Models/Categoria.php
app/Models/Nivel.php
app/Models/Producto.php           ← el más rico (find, findByCategoria, findVendiblesPlan, etc.)
app/Models/ProductoVariante.php
app/Models/ProductoImagen.php
app/Models/PlanLinea.php
app/Models/PrecioUf.php           ← getValorActual() helper
app/Models/TenantConfig.php       ← get(clave), set(clave, valor) helpers
```

---

## Lo que NO está en este sprint

- 🚫 UI para gestionar categorías, niveles, productos → Sprint 1.4
- 🚫 Sembrado de productos genéricos → vos decidiste vacío
- 🚫 Integración real con API de UF → Sprint 2.x
- 🚫 Subida real de imágenes a R2 → Sprint 1.4 (lo del schema queda listo, falta el handler)
- 🚫 Visor 360° → Sprint 2.x
- 🚫 Pantallas de admin (configuración del tenant) → Sprint 1.4

---

## Lo que SÍ valida este sprint

✅ Las 8 tablas existen en `innovium_demo` y `innovium_infinia`
✅ Comando CLI para verificar que los models funcionan
✅ Tests automatizados de los models (CRUD básico)
✅ Migrations versionadas con tracking
✅ FK y constraints aplicadas
✅ `tenant_config` sembrado con valores iniciales mínimos
✅ El user_id en audit_log se respeta cuando los models se usan
