as # ANÁLISIS — Temporadas del Hotel 2 administradas en Reservaciones V2

**Proyectos:** Reservaciones V2 (`CL034RESERVACIONESV2`, base `cl038reservacionesv2`) · Portal público (`CL032BACK`, base `asturias_api`) · Front del portal (`CL032FRONT`)
**Estado:** IMPLEMENTADO Y PROBADO (2026-10-02). Ver **§13** (decisiones finales, archivos, despliegue y resultados de pruebas). Las secciones 0-12 son el análisis previo; donde §13 dice otra cosa, manda §13.
**Fecha:** 2026-10-02
**Parte de:** [PROPUESTA_VISIBILIDAD_HOTEL2_OCUPACION_Y_TEMPORADAS.md](PROPUESTA_VISIBILIDAD_HOTEL2_OCUPACION_Y_TEMPORADAS.md) (Fase 1 y Fase 2 autorizadas)

---

## 0. Resumen ejecutivo

| # | Decisión | Por qué |
|---|---|---|
| 1 | **La Fase 1 se implementa tal como está** (modo `ocupacion`, `> 150`, en el `.env` del portal) | No tiene conflicto con lo nuevo. Es la regla general fuera de temporada |
| 2 | **La tabla maestra `rsv_temporadahotel` se crea en Reservaciones V2.** El portal tiene una **réplica de solo lectura** con la misma estructura funcional | La pantalla debe vivir en V2. La regla corre en el portal **dentro del candado del alta**, así que no puede depender de la red |
| 3 | **No se toca `DisponibilidadController`** (V2) ni el Intermedio | Es un contrato congelado de **solo cupo** (R5). La visibilidad es una política de venta del portal, no cupo |
| 4 | **Canal de réplica propio** V2 → portal (no dentro del canal de catálogos de tarifas) | El hash global del canal de tarifas amarra los 5 catálogos. Una falla de temporadas **detendría la réplica de precios** |
| 5 | **Manda la fecha de llegada.** El Hotel 2 se habilita por temporada si **la primera noche** cae dentro de ella | Es la regla del cliente (ver §2). **Cambia** la regla de "cualquier noche" de la propuesta §4.2 |
| 6 | **La fecha fin es el día sin servicio** (como la fecha de salida): "del 20 al 25" = noches 20 a 24 | Es la regla del cliente. **Cambia** la definición "última noche inclusiva" de la propuesta §4.3 |
| 7 | Dos tipos de regla: **Por fechas** y **Semanal** (de *día* a *día*, con vigencia y **semanas excluidas**) | Petición del cliente |
| 8 | Si se empalman reglas, **gana la opción más abierta**. La única forma de "quitar" una regla semanal es **excluir la semana** | Una temporada solo abre el Hotel 2, nunca lo cierra (propuesta §7) |
| 9 | **Leyenda informativa por regla** (mostrar sí/no + texto editable), en socio **e** invitado, solo si la salida es **posterior** al fin de la temporada | Petición del cliente |

> **Corrección al ejemplo recibido:** en el caso **"22 al 23"** la salida (23) **no** es posterior al 25, así que **no** se muestra la leyenda. Con la configuración del 20 al 25, la leyenda solo aparece en **20→28** y **23→27**. **Confirmado (C1).**

---

## 1. Contexto técnico verificado

| Hecho | Dónde | Consecuencia |
|---|---|---|
| `DisponibilidadController` (V2) es una API **interna de solo lectura** para el Intermedio, con un contrato congelado (`CONTRATO_DISPONIBILIDAD.md §2`): "solo cupo, nunca precio ni paquete (R5)" | `CL034RESERVACIONESV2/app/Http/Controllers/Interno/DisponibilidadController.php:14-24` | Meter temporadas ahí rompería el contrato y obligaría a cambiar **tres** proyectos (V2, Intermedio, portal) |
| La regla de visibilidad se evalúa en el portal en tres puntos: la pantalla, el **alta dentro del candado** y el refresco de contadores | `DisponibilidadService.php:51`, `ReservacionService.php:113`, `:1387` | La regla debe leer datos **locales y en caché**. Una llamada de red dentro del candado alarga la retención y agrega un punto de falla |
| En producción **el portal nunca inicia hacia el privado** (R1). V2 sí sale hacia el portal (catálogos, canal inverso) | `routes/modulos/reservaciones/catalogo.php`, memoria de topología | Para que el portal tenga las temporadas, **V2 debe empujarlas** |
| El canal de catálogos V2 → portal está **encendido** en los dos lados (`REPLICACION_CATALOGOS_ACTIVO=true` / `..._HABILITADA=true`) | V2 `.env:312`, portal `.env:226` | La propuesta §7 decía "apagado"; ya no es cierto |
| Ese canal manda **un solo snapshot** con un **hash global** de 5 catálogos (tipos, paquetes, extras y precios). El receptor recalcula el hash y rechaza si no coincide | `CanonicalizadorCatalogo::hashGlobal`, `AplicarCatalogoPrivadoService::validarHash` | Agregar un catálogo obliga a desplegar los dos lados a la vez y a regenerar el vector dorado. Una falla **frena también los precios** |
| Socio e invitado pasan por el **mismo** `DisponibilidadService::armar()` → `VisibilidadHotelService` | `ReservacionController::disponibilidad` (:89-99) y `::catalogoInvitado` (:234-244) | La regla y la leyenda se implementan **una vez** en el back y se muestran en dos pantallas |
| No existe ninguna tabla ni columna "temporada" en `asturias_api` ni en `cl038reservacionesv2` | Esquemas y `information_schema` | No hay choque de nombres |
| V2 ya tiene un cálculo propio de "temporada Alta/Baja" (viernes a domingo, festivos y paquetes), **solo** para horarios en correos | `app/Library/FSG/ConfiguracionNotificaciones.php:395-427` | Es independiente. **No se toca**; a futuro podría leer esta tabla |
| Zona horaria: `America/Mexico_City` en los dos sistemas | `config/app.php` / `.env` | "Hoy" se calcula igual en ambos |

---

## 2. La regla del cliente, formalizada

### 2.1 Definiciones

- **Noche *n*:** la persona duerme del día *n* al *n+1*.
- **Estancia [llegada, salida):** noches `llegada … salida−1`. La salida **no** es noche.
- **Temporada [inicio, fin):** noches `inicio … fin−1`. **`fin` es el primer día sin servicio** en el Hotel 2, igual que una fecha de salida.
- **Regla de habilitación:** la temporada habilita el Hotel 2 **si y solo si `inicio ≤ llegada < fin`**. La salida puede quedar después del fin.
- **Leyenda:** se muestra si el Hotel 2 está visible, la llegada cae en una regla con leyenda activa **y `salida > finServicio`**. `finServicio` es el fin del tramo continuo de noches en temporada que empieza en la llegada (§4.5).

### 2.2 Verificación contra los ejemplos del cliente (temporada del 20 al 25 de octubre de 2026)

| Estancia | Primera noche | ¿`20 ≤ llegada < 25`? | Hotel 2 por temporada | ¿Salida > 25? | Leyenda | Coincide con el cliente |
|---|---|---|---|---|---|---|
| 18 → 20 | 18 | No | No | No | — | ✅ |
| 18 → 23 | 18 | No | No | No | — | ✅ |
| 18 → 25 | 18 | No | No | No | — | ✅ |
| 18 → 28 | 18 | No | No | Sí | — | ✅ |
| 20 → 23 | 20 | Sí | **Sí** | No | No | ✅ |
| 20 → 25 | 20 | Sí | **Sí** | No (= 25) | No | ✅ |
| 20 → 28 | 20 | Sí | **Sí** | Sí | **Sí** | ✅ |
| 22 → 23 | 22 | Sí | **Sí** | No | **No** | ✅ habilita · ⚠ el cliente lo listó con leyenda |
| 23 → 27 | 23 | Sí | **Sí** | Sí | **Sí** | ✅ |
| 25 → 28 | 25 | No (25 no es noche de la temporada) | No | — | — | ✅ |

Los 10 casos quedan como **vector de pruebas obligatorio** en los dos proyectos (§9).

### 2.3 Consecuencias que el cliente debe conocer

1. **"No se habilita" significa "la temporada no lo habilita".** La regla general sigue aplicando: si en 18 → 23 el Hotel 1 pasa de 150 ocupadas, el Hotel 2 **sí** aparece, por ocupación. La válvula (Hotel 1 agotado) también lo abre.
2. **18 → 23 pierde la temporada aunque duerma 3 noches dentro.** Es una decisión consciente del cliente. Si quieren capturar esas llegadas, basta con adelantar el inicio de la temporada.
3. **20 → 28 duerme 3 noches en el Hotel 2 sin servicio.** La regla solo decide **qué se publica**. Si el Hotel 2 **cierra físicamente** después del 25, eso se hace con un **bloqueo en Reservaciones V2**. Entonces el cupo de esas noches es 0 y el Hotel 2 aparece sin disponibilidad para esa estancia. La leyenda no sustituye a un bloqueo.

---

## 3. ¿Dónde se crea la tabla? Análisis de alternativas

| Alternativa | Ventajas | Desventajas | Veredicto |
|---|---|---|---|
| **A.** Solo en el portal; la pantalla de V2 escribe por API remota | Sin réplica | V2 dependería del portal hasta para **listar**; transacciones entre dos sistemas; bitácora partida; contradice "el portal nunca crea catálogos" (D7/D8). En producción V2 sale por Internet con IP dinámica | ❌ |
| **B.** Solo en V2; el portal la consulta en cada petición por el feed (`DisponibilidadController` → Intermedio) | Siempre al día | Rompe el contrato congelado de cupo (R5); cambia **tres** proyectos; mete red y latencia **dentro del candado del alta**; el Intermedio cachea por fecha | ❌ |
| **C1.** Maestra en V2 y réplica en el portal **dentro del canal de catálogos** existente | Reusa todo el canal | El hash global acopla las temporadas con las **tarifas**: un despliegue a destiempo o una fila mala **detiene la réplica de precios** (422 `hash_no_coincide`). Hay que tocar el aplicador de 986 líneas, el canonicalizador duplicado byte a byte y el vector dorado | ⚠ Posible, no recomendada |
| **C2.** Maestra en V2 y réplica en el portal **por un canal propio y pequeño** con la misma seguridad | Aislamiento total: si falla, las tarifas no se enteran. Despliegue independiente. Reusa `HmacSigner`, el middleware `seguridadEscritura:cuerpo` y el patrón snapshot idempotente. Es el mismo criterio que ya separa `catalogos` de `inversa` en `config/replicacion.php` ("un interruptor compartido es un interruptor que un día enciende lo que nadie pidió") | Algo de plomería nueva (job, endpoint, estado) | ✅ **Recomendada** |

**Conclusión:** la tabla **se crea en los dos lados con roles distintos**:

- **Reservaciones V2** (`cl038reservacionesv2.rsv_temporadahotel` + `rsv_temporadahotelexclusion`): **maestra**. Pantalla, validaciones, bitácora y permisos.
- **Portal** (`asturias_api.rsv_temporadahotel` + `rsv_temporadahotelexclusion`): **réplica de solo lectura**, sin pantalla ni rutas de escritura de personas. La lee `VisibilidadHotelService`.

`DisponibilidadController` no participa: decide **cuánto** hay, no **qué se publica**. Además, el personal que vende en V2 no debe verse afectado (propuesta E16).

---

## 4. Modelo de datos

### 4.1 `rsv_temporadahotel` (V2, maestra)

Se usan ENUM con palabras completas (`'Activo'`, …), como `rsv_extras.extraEstado` y `seg_ruta.rutaEstado`. Así no hacen falta entradas en `enum_mappings` / `char_mappings` ni un trait `*Attributes` (ARQUITECTURA_PROYECTO V2 §ENUM).

| Campo | Tipo | Nulo | Default | Notas |
|---|---|---|---|---|
| `idTemporadaHotel` | int AI | No | — | PK |
| `temporadaHotelAlias` | varchar(75) | No | — | "Temporada alta octubre", "Fines de semana 2027" |
| `temporadaHotelTipo` | enum('Fechas','Semanal') | No | `'Fechas'` | |
| `temporadaHotelFechaInicio` | date | No | — | **Fechas:** primera noche. **Semanal:** inicio de vigencia |
| `temporadaHotelFechaFin` | date | No | — | **Fechas:** primer día **sin** servicio (no es noche). **Semanal:** fin de vigencia, misma convención |
| `temporadaHotelDiaInicio` | tinyint | Sí | NULL | Solo Semanal. ISO-8601: 1 = lunes … 7 = domingo. Día de la **primera noche** del bloque |
| `temporadaHotelDiaFin` | tinyint | Sí | NULL | Solo Semanal. Día **sin servicio** (como la salida). Viernes→Domingo = noches de viernes y sábado |
| `temporadaHotelSiempreHabilitado` | tinyint(1) | No | 1 | 1 = Hotel 2 abierto. 0 = abre por `temporadaHotelUmbral` |
| `temporadaHotelUmbral` | int | Sí | NULL | Habitaciones **ocupadas** del Hotel 1 (`>`). NULL si siempre habilitado |
| `temporadaHotelMostrarLeyenda` | tinyint(1) | No | 1 | Muestra el aviso si la salida es posterior al fin |
| `temporadaHotelLeyenda` | varchar(300) | Sí | NULL | Texto del aviso. NULL = texto por defecto (§6.3) |
| `temporadaHotelEstado` | enum('Activo','Inactivo','Eliminado') | No | `'Activo'` | Solo `Activo` participa |
| `temporadaHotelFecha` | datetime | No | — | Captura |
| `temporadaHotelFechaModificacion` | datetime | Sí | NULL | |
| `idUsuario` | int | No | — | FK al usuario de V2 (último que modificó) |

**CHECK / validación:**

- `FechaFin > FechaInicio`: al menos una noche. **Cambia** el `>=` de la propuesta.
- **Fechas:** `DiaInicio` y `DiaFin` son NULL.
- **Semanal:** los dos entre 1 y 7 y distintos. Noches por bloque = `(DiaFin − DiaInicio) mod 7`, de 1 a 6.
- `SiempreHabilitado = 1` ⇔ `Umbral IS NULL`.
- Los CHECK requieren MySQL 8.0.16 o superior. **Pre-producción tiene MySQL 5.7** (según el script 06 de V2), así que ahí **la garantía real es el Form Request**.

### 4.2 `rsv_temporadahotelexclusion` (V2, maestra)

| Campo | Tipo | Notas |
|---|---|---|
| `idTemporadaHotelExclusion` | int AI | PK |
| `idTemporadaHotel` | int | FK a `rsv_temporadahotel` |
| `temporadaHotelExclusionAnio` | smallint | **Año ISO** de la semana (no el año calendario) |
| `temporadaHotelExclusionSemana` | tinyint | 1-53, semana ISO-8601 (lunes a domingo) |

Índice único `(idTemporadaHotel, temporadaHotelExclusionAnio, temporadaHotelExclusionSemana)`. Solo aplica a reglas **Semanal**.

**¿Por qué tabla hija y no JSON?** La ARQUITECTURA parte de tablas con campos nombrados (§1-2). La tabla hija permite unicidad y FK, y se replica con la misma llave natural.

**¿Por qué año ISO + semana y no "semana 10" a secas?** El número de semana es ambiguo:

- El viernes **1 de enero de 2027 pertenece a la semana 53 de 2026**.
- En México mucha gente cuenta las semanas de domingo a sábado, no de lunes a domingo.

Por eso la pantalla **nunca pide escribir un número**: muestra la lista de semanas de la vigencia **con sus fechas** y el usuario las marca:

```
☐ Semana 10 · 8 al 14 de marzo de 2027
☐ Semana 11 · 15 al 21 de marzo de 2027
☐ Semana 20 · 17 al 23 de mayo de 2027
```

**¿A qué semana pertenece un bloque que cruza el lunes** (sábado → martes)? A la semana de **su primera noche**. Excluir esa semana quita el bloque completo, no media estancia.

### 4.3 Réplica en el portal

Las mismas dos tablas, con estas diferencias:

- Solo las columnas **funcionales**. **Sin `idUsuario`** ni FK a `seg_usuario`: es el usuario de **otro** sistema, y la FK haría fallar el upsert (error 1452).
- **Sin** `temporadaHotelFecha` NOT NULL.
- Mismos `id` que V2 (D8: los ids vienen del maestro).
- Se replican **todas las filas `Activo` con fin ≥ hoy**. Lo inactivo, eliminado o vencido no viaja, y el receptor lo borra de su réplica.

---

## 5. Evaluación en el portal

### 5.1 Orden de la decisión (Fase 1 + Fase 2)

```
1. regla inactiva (ACTIVA=false) / configuración inválida   → visible
2. la estancia no tiene tipos del Hotel 2                    → nada que ocultar
3. la captura ya tiene tipos del Hotel 2                     → visible
4. NUEVO · la LLEGADA cae en una regla "siempre habilitado"  → visible  (motivo temporada_siempre_habilitado)
5. inventario del Hotel 1 = 0                                → visible
6. válvula: Hotel 1 agotado (real)                           → visible
7. umbral efectivo:
     umbral = el del .env (Fase 1)
     si la LLEGADA cae en reglas con umbral: umbral = min(.env, umbrales de esas reglas)
     visible si ocupadas = inventario − disponibles sin Temporales > umbral
```

Los pasos 1-3 y 5-6 **no cambian**. La Fase 1 solo cambia la comparación del paso 7.

### 5.2 Compatibilidad entre Fase 1 y Fase 2

| Riesgo | Cómo se evita |
|---|---|
| El umbral de temporada está en **ocupadas**; si el `.env` sigue en `cantidad`, se mezclarían unidades | El umbral de temporada solo aplica con `MODO=ocupacion`. En otro modo se ignora con `Log::warning`. La opción **siempre habilitado** funciona en cualquier modo |
| Umbral de temporada **mayor** que el general (200 contra 150): cerraría más | Se usa el **menor**. La temporada nunca cierra. V2 lo advierte al capturar (§6.2) con el umbral general que reporta el portal |
| Umbral de temporada ≥ inventario del Hotel 1: nunca abriría | V2 lo rechaza; conoce el inventario (`rsv_tipohabitacion`, Hotel 1 = `'V'`) |
| La tabla réplica no existe o falla la BD/caché | Se evalúa **sin temporadas** (solo Fase 1) + `Log::error`. Una falla de la tabla nueva **no cambia la regla vigente** |
| Desactivar la Fase 2 de emergencia | Interruptor nuevo `RESERVACION_TEMPORADAS_HOTEL_ACTIVA` (por defecto `true`). En `false`, el portal se comporta como en la Fase 1 |
| La pantalla ofrece el Hotel 2 y el alta lo rechaza | Pantalla y alta usan el mismo servicio y la misma caché. Si alguien cambia una temporada entre las dos, el alta responde con el mensaje que ya existe y la pantalla se refresca (igual que E11) |

### 5.3 Empalmes (solapamientos)

| Caso | ¿Se permite? | Resolución |
|---|---|---|
| Dos reglas **Fechas** activas que comparten una noche | ❌ No. Condición de empalme semiabierta: `a.inicio < b.fin AND b.inicio < a.fin` | Error claro: *"Ya existe 'Temporada alta octubre' del 20 al 25 de octubre"* |
| Dos reglas **Fechas** contiguas (`a.fin = b.inicio`) | ✅ Sí | No se empalman. La leyenda las **une** (el servicio sigue) |
| Dos reglas **Semanal** activas con vigencias empalmadas | ❌ No | *"Ya hay una regla semanal vigente en esas fechas; edítala"*. Para "jueves a domingo en verano y viernes a domingo el resto del año" se crean vigencias consecutivas |
| **Fechas** sobre **Semanal** (por ejemplo, Semana Santa dentro de "fines de semana 2027") | ✅ Sí | En cada noche **gana la opción más abierta**: siempre > umbral menor > umbral mayor. La pantalla muestra una **advertencia**, no un error |
| Semana excluida en la regla semanal | — | Esa semana la regla semanal **no aplica** y rige la regla general (Fase 1). **No cierra** el Hotel 2. Una regla Fechas en esa semana sí aplica |
| **Reactivar** una regla inactiva que ahora se empalma | ❌ | `cambiarEstado` a `Activo` **repite la validación de empalmes**. La propuesta no lo cubría |
| Dos usuarios guardando al mismo tiempo | — | `lockForUpdate` sobre filas que **todavía no existen** no evita inserciones simultáneas (lectura fantasma). Se usa un **candado con nombre** (`Cache::lock('temporadahotel:guardar')`) alrededor de "validar + guardar". **Corrige** la propuesta §4.3 |

### 5.4 Regla semanal: cómo se calcula una noche

Para una noche *n* y una regla semanal con vigencia `[inicio, fin)`, `DiaInicio = di` y `DiaFin = df`:

```
largo        = (df − di) mod 7                      (1..6 noches)
desplaz      = (diaISO(n) − di) mod 7
cubre(n)     = inicio ≤ n < fin
               AND desplaz < largo
               AND semanaISO(n − desplaz) NO está excluida
finBloque(n) = min(n − desplaz + largo, fin)
```

| Configuración | Noches | Llegadas que habilitan | Día sin servicio |
|---|---|---|---|
| Viernes → Domingo | vie, sáb | viernes y sábado | domingo |
| Jueves → Domingo | jue, vie, sáb | jueves a sábado | domingo |
| Viernes → Lunes (puente) | vie, sáb, dom | viernes a domingo | lunes |
| Sábado → Martes (cruza de semana) | sáb, dom, lun | sábado a lunes | martes |

**Importante para el cliente:** "de viernes a domingo" usa **la misma convención que las fechas**: el domingo es el día de salida. Si quieren que **llegar en domingo** también habilite, la regla es "viernes → lunes". La pantalla lo dice en texto (§6.2).

### 5.5 Fin de servicio para la leyenda

```
n = llegada
mientras n < salida y alguna regla activa cubra n:  n = n + 1
finServicio = n
leyenda si: Hotel 2 visible  Y  regla de la llegada con leyenda activa  Y  finServicio < salida
```

- Se unen reglas contiguas o empalmadas: si una regla de fechas termina el 25 y una semanal empieza el 25, el servicio **no se corta** y no hay aviso.
- El ciclo nunca pasa de las noches de la estancia (máximo de noches de `ReglasFechaService`).
- **Texto y bandera:** si la llegada cae en una regla Fechas y en una Semanal, manda la de **Fechas** (es la más específica).
- La leyenda **no** depende del motivo por el que se ve el Hotel 2. Así también aparece cuando el socio ya tiene apartado el Hotel 2 (paso 3) o cuando lo abrió la ocupación. Si el personal no la quiere en una regla, la apaga.

### 5.6 Estructura del código en el portal

| Componente | Responsabilidad |
|---|---|
| `app/Services/Reservaciones/TemporadaHotelCalendario.php` (**nuevo, puro**) | `cubre()`, reglas que cubren la llegada, `umbralTemporada()`, `finServicio()`. Sin BD: recibe las reglas como arreglo |
| `VisibilidadHotelService` | Paso 4 nuevo y umbral efectivo. `evaluar()` **sigue siendo puro**: recibe las temporadas por parámetro. La lectura con caché va en `tiposOcultos()` / `tiposOcultosParaEstancia()`. Nuevo `avisoHotel()` |
| `TemporadaHotel` / `TemporadaHotelExclusion` (modelos réplica) | `vigentes()` (con caché `visibilidad_hotel:temporadas`, TTL de respaldo de 5 min) y `reemplazar()` |
| `DisponibilidadService::armar()` | Devuelve una tercera llave `avisoHotel` (`null` o `{fechaFinServicio, texto}`) |
| `ReservacionController::disponibilidad` / `::catalogoInvitado` | Pasan `avisoHotel` junto a `hoteles` (:110 y :257) |

`TemporadaHotelCalendario` es **idéntico** en V2 (vista previa) y en el portal (decisión), igual que `PaqueteServiceBase` y `CanonicalizadorCatalogo`. Si cambia el criterio, se cambian los dos en el mismo commit y los dos corren el **mismo vector de casos** (§9).

---

## 6. Interfaz en Reservaciones V2 (personal no experto)

Patrón de V2: Blade montador + Vue (Modo B), `useCrud`, `CrudLayout`, `DynamicTable`, `BaseModal`, `BaseInput`, `BaseButton`, `ConfirmModal` y `AlertNotification`. La plantilla de lógica es el módulo **Bloqueo** (`rsv_bloqueo`), con nombres canónicos (`GuardarFormRequest`, `Resources/.../TemporadaHotel/ListaResource`).

### 6.1 Lista

| Nombre | Cuándo | Hotel 2 | Aviso | Estado | Publicado en el portal |
|---|---|---|---|---|---|
| Temporada alta octubre | Del mar 20 al dom 25 oct 2026 | Siempre abierto | Sí | Activo | ✅ |
| Fines de semana 2027 | Viernes a domingo · todo 2027 · 3 semanas excluidas | Siempre abierto | Sí | Activo | ✅ |

- Por defecto se ven las **vigentes y futuras**, con un filtro para ver las pasadas.
- Arriba hay un recuadro con la **regla general del portal** ("Fuera de temporada, el Hotel 2 se abre cuando el Hotel 1 tiene más de 150 habitaciones ocupadas"), que se lee del portal (§7.3).

### 6.2 Formulario: tres preguntas, en lenguaje llano

```
1. ¿Cómo se llama?            [ Temporada alta octubre            ]

2. ¿Cuándo?                   (•) Unas fechas      ( ) Cada semana

   Unas fechas →  Desde [ 20/10/2026 ]  Hasta [ 25/10/2026 ]
                  ℹ Se abre para quienes LLEGUEN del 20 al 24 de octubre.
                    El 25 ya no hay servicio en el Hotel 2.

   Cada semana →  De [ Viernes ▾ ] a [ Domingo ▾ ]
                  Vigente desde [ 01/01/2027 ] hasta [ 01/01/2028 ]
                  ℹ Se abre para quienes LLEGUEN viernes o sábado.
                    El domingo ya no hay servicio en el Hotel 2.
                  ¿Alguna semana sin temporada?  [ Elegir semanas ▾ ]
                    ☑ Semana 10 · 8 al 14 de marzo de 2027
                    ☑ Semana 11 · 15 al 21 de marzo de 2027
                    ☑ Semana 20 · 17 al 23 de mayo de 2027

3. ¿Cómo se abre el Hotel 2?  (•) Siempre abierto en estas fechas
                              ( ) Cuando el Hotel 1 tenga más de [ ___ ] habitaciones ocupadas
                                  (Hotel 1 tiene hoy 180 habitaciones)

   Aviso a socios e invitados
   [✓] Avisar si su salida es después del fin de la temporada
       Texto: [ Si te hospedas en el Hotel 2 después de esa fecha, cualquier
                servicio tendrás que solicitarlo en el Hotel 1.            ]
       Vista previa: ℹ El Hotel 2 da servicio hasta el domingo 25 de octubre de 2026. Si te…
```

**Ayudas que evitan errores:**

| Ayuda | Qué resuelve |
|---|---|
| Frase automática "Se abre para quienes LLEGUEN…" | Que el usuario entienda la regla de llegada y el día sin servicio sin leer un manual |
| **"Probar una estancia"**: llegada y salida → *"Hotel 2: se abre por temporada ✓ · Se mostrará el aviso ✓"* | El usuario valida sus propios ejemplos (como los de §2.2) antes de guardar |
| **"Próximas fechas"** (regla semanal): las 8 próximas ocurrencias, con las excluidas tachadas | Ver el efecto real de "viernes a domingo" y de las exclusiones |
| La fecha del aviso se pone sola; el usuario solo edita el texto | Sin variables ni marcadores que confundan |
| Advertencia (no error) si se empalma con una regla semanal | Que sepa que "gana la opción más abierta" |
| Errores en español junto al campo (`validationErrors`) | ARQUITECTURA §13 |

Estas ayudas usan el mismo `TemporadaHotelCalendario` que el portal, así que lo que ve el personal es **exactamente** lo que hará el portal.

### 6.3 Texto por defecto de la leyenda

> **El Hotel 2 da servicio hasta el {domingo 25 de octubre de 2026}.** Si te hospedas en el Hotel 2 después de esa fecha, cualquier servicio tendrás que solicitarlo en el Hotel 1.

El título con la fecha lo arma el sistema. El cuerpo es el `temporadaHotelLeyenda` editable, o este texto si se deja vacío.

### 6.4 Validaciones (`GuardarFormRequest` + controlador)

| Regla | Mensaje |
|---|---|
| Hasta > Desde | "La fecha 'hasta' es el día en que ya no hay servicio; debe ser posterior a 'desde'." |
| Hasta ≥ hoy (no capturar temporadas pasadas) | "Esta temporada ya terminó." |
| Vigencia de la regla semanal ≤ 2 años | "La vigencia no puede ser mayor a dos años." |
| Día inicial ≠ día final | "Elige días distintos (por ejemplo, de viernes a domingo)." |
| Semanas excluidas dentro de la vigencia | "La semana 20 de 2027 está fuera de la vigencia." |
| Umbral obligatorio si no es "siempre"; 0 ≤ umbral < inventario del Hotel 1 | "Con ese número el Hotel 2 nunca se abriría (el Hotel 1 tiene 180)." |
| Empalme Fechas–Fechas o Semanal–Semanal (también al **reactivar**) | Nombre y fechas de la regla con la que choca |
| Editar una regla que ya terminó | No se permite: queda de solo lectura |

### 6.5 Bitácora, permisos y menú

- **Bitácora:** `BitacoraReservaciones::agregar` con 3 acciones nuevas en `seg_accionreservaciones` (hoy el máximo es 116): agregar, editar y cambiar estado de una temporada del Hotel 2. El detalle guarda el antes y el después.
- **Permisos:** filas en `seg_ruta` (`reservaciones.temporadaHotel.{index,listar,guardar,detalle,cambiarEstado,probar,publicar}`) y `seg_perfilruta`, con un script idempotente según la plantilla `database/sql/replicacion/06_seg_ruta_catalogoreplicacion-1.sql`.
- **Menú:** hijo de "Catálogos" (`idMenu=207`, `menuSistema='4'`) + `seg_perfilmenu`.
- ⚠ En local, `Permiso::handle` deja pasar todo (`app/Http/Middleware/Permiso.php:27`). Sin el script de permisos, **en el servidor la pantalla responde 403**.

---

## 7. Canal de réplica "temporadas" (V2 → portal)

### 7.1 Diseño

| Pieza | V2 (emisor) | Portal (receptor) |
|---|---|---|
| Disparo | Después del `commit` de `guardar` / `cambiarEstado` → job `ReplicarTemporadasHotelPortal` (`ShouldBeUnique`, cola `replicacion-portal`, retardo de unos segundos) | — |
| Respaldo | Cron cada 5 min: compara su hash con el que reporta el portal (`GET /estado`) y reenvía si difieren. También corrige que pase la medianoche (lo vencido sale del snapshot) | — |
| Carga | Snapshot **completo**: `{version:1, generadoEn, origen, hash, temporadas:[…], exclusiones:[…]}`. Docenas de filas | `POST /reservaciones/temporada-hotel/aplicar` (`seguridadEscritura:cuerpo`, `throttle`) |
| Aplicación | — | Valida forma y hash → transacción: **reemplaza** el contenido de las dos tablas → guarda hash y `generadoEn` → **después del commit** `Cache::forget('visibilidad_hotel:temporadas')` |
| Orden | El job es único y lee la BD al momento de enviar | Ignora un snapshot con `generadoEn` **anterior** al último aplicado (`409 version_anterior`) |
| Estado | La pantalla muestra "Publicado en el portal ✓ / Pendiente ⏳ / Error ✖" y un botón **Publicar ahora** | `GET /reservaciones/temporada-hotel/estado` → `{hash, generadoEn, filas, reglaGeneral:{activa, modo, umbral}}` |
| Cortacircuitos | **No** usa el `CircuitBreakerPortal` compartido, para que una falla de temporadas no frene reservaciones ni tarifas. Si falla, queda pendiente y el cron reintenta | — |
| Interruptores | `REPLICACION_TEMPORADAS_ACTIVO` | `REPLICACION_TEMPORADAS_HABILITADA` (si está apagado responde 404, igual que los otros canales) |

**¿Por qué reemplazar el contenido y no hacer upsert + apagado?** En el portal nadie referencia estas filas (las ventas no guardan `idTemporadaHotel`). Reemplazar dentro de una transacción es lo más simple, siempre converge y no deja filas huérfanas.

### 7.2 Qué pasa si el canal se cae

- El portal **sigue usando las últimas temporadas recibidas**: seguro, aunque no estén al día.
- Una temporada nueva no llega hasta que el canal se recupera. **Por eso la pantalla muestra el estado de publicación**: el usuario ve "Pendiente" y no supone que ya está en línea.
- El aviso por correo reutiliza el monitoreo existente (R19).

### 7.3 Regla general visible en V2

`/estado` devuelve el modo y el umbral del `.env` del portal (Fase 1), así que V2 puede:

- mostrar al usuario la regla general en palabras;
- advertir si captura un umbral de temporada **mayor o igual** al general (no tendría efecto);
- no ofrecer la opción "Cuando el Hotel 1 tenga más de…" si el portal no está en modo `ocupacion`.

---

## 8. Front del portal (CL032FRONT)

| Pieza | Cambio |
|---|---|
| `src/components/alert/AvisoInformativo.vue` (**nuevo, reutilizable**) | Recuadro en línea, tono informativo (`--info`, `bi bi-info-circle`), props `titulo` y `mensaje`. `AvisoBloqueo` es rojo con `role="alert"` y `AlertNotification` es un toast que se cierra solo: ninguno sirve para un aviso permanente (ARQUITECTURA §10.1 #6) |
| `stores/reservaciones/reservacionStore.js` | Estado `avisoHotel`. Se llena en `fetchDisponibilidad` (:593-599) y en `fetchCatalogoInvitado` (:666-676); se limpia en `reset()` |
| `DisponibilidadSocio.vue` | Aviso dentro del bloque del Hotel 2, bajo `hotel-titulo` (:307-322) |
| `HabitacionesInvitado.vue` | Aviso bajo la píldora o pestaña del Hotel 2 (escritorio :197-210, móvil :215-237) |

El texto se pinta con interpolación de Vue (escapada), sin `v-html`: el texto lo escribe el personal y no debe poder inyectar HTML.

En el socio, cambiar de fechas ya vuelve a consultar la disponibilidad, así que el aviso se recalcula solo. En el invitado, las fechas son las de la solicitud y no cambian.

---

## 9. Plan de pruebas

### 9.1 Vector común (los dos proyectos)

Un archivo de casos `temporada_hotel_casos.json` **idéntico** en `CL032BACK/tests/Fixtures/` y `CL034RESERVACIONESV2/tests/Fixtures/`. Lo ejecuta la prueba unitaria de `TemporadaHotelCalendario` en cada proyecto. Contiene:

| Grupo | Casos |
|---|---|
| Ejemplos del cliente | Los 10 de §2.2 (habilita y leyenda) |
| Bordes de fechas | Temporada de una sola noche; llegada = fin − 1; llegada = fin; estancia de una noche |
| Semanal | Viernes→Domingo, Jueves→Domingo, Viernes→Lunes, Sábado→Martes (cruza de semana); llegada en domingo; vigencia que corta a media semana |
| Exclusiones | Semanas 10, 11 y 20 de 2027; semana 53 de 2026 (1 de enero de 2027); bloque que cruza el lunes de una semana excluida |
| Empalmes | Fechas sobre Semanal (gana siempre sobre umbral; gana el menor umbral); reglas contiguas que unen el fin de servicio |
| Leyenda | Salida = fin (no); salida > fin (sí); `MostrarLeyenda = 0` (no); texto por defecto contra texto propio |

### 9.2 Portal (PHPUnit, SQLite en memoria: no toca MySQL)

| Prueba | Qué cubre |
|---|---|
| `Unit/Reservaciones/TemporadaHotelCalendarioTest` | El vector común |
| `Unit/Reservaciones/VisibilidadHotelServiceTest` (ampliada) | Las 5 pruebas de Fase 1 (propuesta §3.4) y el orden de §5.1: siempre en la llegada, umbral efectivo = mínimo, umbral de temporada ignorado si el modo no es `ocupacion`, regla inactiva y empalmes. Los 17 casos actuales siguen pasando |
| `Feature/Reservaciones/VisibilidadHotelTest` (ampliada) | `armar()` devuelve `avisoHotel`; el alta respeta la temporada dentro del candado; tabla inexistente → regla de Fase 1 + log; interruptor de temporadas apagado |
| `Feature/Reservaciones/TemporadaHotelReplicaTest` (nueva) | Aplicar el snapshot; idempotencia (mismo hash → `sin_cambios`); reemplazo; `generadoEn` viejo → 409; hash incorrecto → 422; la caché se invalida; `/estado` |
| Front: Vitest (`__tests__`) | El store mapea `avisoHotel`; `DisponibilidadSocio` y `HabitacionesInvitado` muestran o no el aviso; `AvisoInformativo` |

### 9.3 Reservaciones V2 (PHPUnit contra `cl038reservacionesv2_testing`)

Patrón de V2: filas propias, borradas en `tearDown`, FormRequest con `validateResolved()`, controlador invocado directo, `Http::fake` y `Queue::fake`.

| Prueba | Qué cubre |
|---|---|
| `Unit/TemporadaHotelCalendarioTest` | El vector común |
| `Feature/TemporadaHotelGuardarTest` | Todas las validaciones de §6.4, empalmes (también al reactivar), bitácora y despacho del job después del commit |
| `Feature/TemporadaHotelReplicaTest` | Forma y firma del snapshot (`Http::fake`); solo `Activo` con fin ≥ hoy; estado Pendiente → Publicado; reintento del cron cuando difiere el hash |
| `Feature/TemporadaHotelProbarTest` | "Probar una estancia" con los 10 ejemplos del cliente |

V2 **no tiene** infraestructura de pruebas JS (no hay Vitest). La pantalla se valida con una lista de QA manual. Agregar Vitest a V2 queda fuera de alcance salvo que lo pidas.

### 9.4 Prueba de punta a punta (pre-producción)

1. Crear en V2 "del 20 al 25 de octubre de 2026, siempre abierto, con aviso" → en segundos aparece "Publicado ✓".
2. En el portal, como socio, probar las 10 estancias de §2.2 con el Hotel 1 por debajo de 150 ocupadas: visible / oculto / aviso según la tabla.
3. Repetir 20 → 28 y 23 → 27 en la pantalla de invitado.
4. Desactivar la temporada en V2 → la siguiente búsqueda ya no muestra el Hotel 2 (si no hay ocupación) y la captura en curso conserva lo apartado.
5. Regla semanal viernes→domingo con la semana actual excluida → comprobar llegada en viernes con y sin exclusión.
6. Apagar el receptor → V2 muestra "Pendiente"; encenderlo → converge solo.

---

## 10. Orden de implementación y liberación

| Paso | Proyecto | Contenido | Comportamiento después del paso |
|---|---|---|---|
| 1 | Portal | **Fase 1**: modo `ocupacion` + pruebas. `.env` → `MODO=ocupacion`, `UMBRAL=150` | Regla del cliente "> 150" |
| 2 | Portal | DDL réplica + `TemporadaHotelCalendario` + paso 4 / umbral efectivo + `avisoHotel` + receptor `/aplicar` y `/estado` + pruebas | Igual que el paso 1 (réplica vacía) |
| 3 | Front | `AvisoInformativo` + store + dos pantallas + Vitest | Igual (nadie manda aviso todavía) |
| 4 | V2 | DDL maestra + modelos + Form Requests + Resources + controlador + rutas + Blade/Vue/store + bitácora + permisos/menú + emisor + cron + pruebas | El personal captura temporadas y el portal las aplica |

El receptor va **antes** que el emisor. Si V2 se adelanta, sus envíos reciben 404 y quedan "Pendiente" **sin afectar tarifas ni reservaciones**: esa es la ventaja de C2.

**Rollback:**

- Fase 2: `RESERVACION_TEMPORADAS_HOTEL_ACTIVA=false` en el portal, o desactivar las reglas en V2.
- Fase 1: `.env` de regreso a `MODO=cantidad`, `UMBRAL=170`.
- Las tablas pueden quedarse: nadie las lee.

---

## 11. Diferencias con la propuesta original

| Propuesta | Este análisis | Motivo |
|---|---|---|
| §4.1 `FechaFin` = última noche, inclusiva | `FechaFin` = primer día sin servicio (exclusiva) | Regla del cliente ("25 → 28 no se habilita") |
| §4.2 Basta con **una** noche en temporada | Manda **la llegada** | Regla del cliente ("18 → 23 no se habilita") |
| §4.2 Umbral = el menor de las noches | Umbral = mínimo entre el general y las reglas **de la llegada** | Consecuencia de la regla de llegada |
| §4.3 No se permite ningún empalme | Fechas–Fechas y Semanal–Semanal no; Fechas sobre Semanal sí (gana la más abierta) | Las reglas semanales se empalman con fechas especiales por naturaleza |
| §4.3 `lockForUpdate` sobre las filas que se empalman | Candado con nombre | `lockForUpdate` no evita inserciones simultáneas (fantasmas) |
| §4.3 FK `idUsuario` → `seg_usuario` del portal | Sin `idUsuario` en la réplica | Es el usuario de otro sistema; rompería el upsert |
| §4.4-4.5 Pantalla Blade en el panel de CL032BACK | Pantalla Vue en Reservaciones V2; el portal no tiene pantalla | Petición del cliente; D7 (el portal no crea catálogos) |
| §4.4 Bitácora 26/27 en `rsv_bitacoraaccion` del portal | `seg_accionreservaciones` de V2 | La escritura ocurre en V2 |
| §7 Canal de catálogos "apagado" | Encendido; se recomienda canal **propio** | Aislamiento de las tarifas |
| — | Regla **Semanal** con semanas excluidas, leyenda configurable, "Probar una estancia" | Peticiones nuevas del cliente y facilidad de uso |

---

## 12. Puntos para confirmar con el cliente antes de programar

| # | Pregunta | Supuesto si no responde |
|---|---|---|
| C1 | En "22 al 23", ¿de verdad quieren el aviso? (la salida no pasa del 25) | **No** se muestra: solo si la salida > fin |
| C2 | "De viernes a domingo", ¿el domingo es día de salida (noches de viernes y sábado)? | **Sí**, la misma convención que las fechas. Para incluir la noche del domingo: "viernes → lunes" |
| C3 | Si una fecha especial se empalma con la regla semanal, ¿gana la opción más abierta? | **Sí** |
| C4 | ¿Las semanas excluidas se cuentan de lunes a domingo (norma ISO)? | **Sí**, y la pantalla siempre muestra las fechas |
| C5 | La regla, ¿aplica solo al portal (no a la venta del personal en V2)? | **Sí** (E16) |
| C6 | Fase 1, decisiones D1-D3 de la propuesta (`>` estricto, Temporales no cuentan, bloqueadas sí cuentan) | Los supuestos de la propuesta |

> **Respuestas del cliente (2026-10-02):** C1, C2 y C3 confirmados con el supuesto de la tabla.

---

## 13. Implementación entregada (2026-10-02)

### 13.1 Decisiones finales (cambian lo descrito arriba)

| Tema | Final |
|---|---|
| Edición en V2 | **No hay editar**: solo **agregar** y **eliminar** (baja lógica `Eliminado`). Para cambiar una temporada se elimina y se agrega otra. Las terminadas quedan como historial de solo lectura |
| Réplica | **Al instante y sin colas**: `guardar` / `eliminar` escriben, publican el snapshot en el portal **dentro de la misma transacción** y solo hacen commit si el portal confirma. Si el portal rechaza o no responde → rollback y mensaje "No se guardó ningún cambio" (502). Si no se sabe si el portal alcanzó a aplicar (timeout, 5xx) se **compensa** reenviando el estado ya revertido |
| Cron / job | **No hay.** En su lugar: **Validar con el portal** (compara presentes y futuras, por hash y fila por fila) y **Sincronizar ahora** (reenvía el snapshot completo; idempotente) |
| Regla automática de 150 | Nuevo interruptor `RESERVACION_VISIBILIDAD_HOTEL_AUTOMATICA` (portal). En `false` el Hotel 2 solo se abre por temporada, válvula o captura con Hotel 2; fuera de temporada queda **oculto**. Distinto de `ACTIVA=false`, que muestra los dos hoteles |
| Umbral de temporada | Siempre en **ocupadas**. Se evalúa aparte de la regla automática y basta con que abra una (gana la más abierta sin mezclar unidades). Con la automática apagada, usa solo su número |
| Caso 26→29 | No habilita por temporada (la llegada queda fuera); solo la regla automática o la válvula |
| Estados V2 | `enum('Activo','Eliminado')` (sin `Inactivo`: no hay reactivación) |
| Concurrencia V2 | Candado con nombre (`Cache::lock('temporadahotel:escritura')`) alrededor de "revalidar empalmes + escribir + publicar" |
| Tipos que no se venden en línea | `RESERVACION_TIPOS_NO_VENTA_LINEA=9` (portal; ids separados por coma; 9 = ESPECIALES H DOS). Un solo punto de lectura (`DisponibilidadService::tiposNoVentaLinea()`) usado en: la pantalla (socio e invitado, aunque la regla del Hotel 2 esté apagada), el alta (409 "Este tipo de habitación no se vende en línea.", aunque llegue a mano), el refresco de contadores (0) y la válvula (un tipo no vendible del Hotel 1 no cuenta como "algo que vender"). No cambia cupo ni la ocupación que mide la regla de 150 |

### 13.2 Archivos

**Compartidos (idénticos byte a byte en los dos proyectos):**
`app/Library/FSG/TemporadaHotelCalendario.php` · `app/Library/FSG/TemporadaHotelCanonico.php` · `tests/Fixtures/temporada_hotel_casos.json`

**Portal (CL032BACK):**

| Tipo | Archivo |
|---|---|
| DDL réplica | `database/sql/rsv_temporadahotel.sql` + `database/migrations/2026_10_02_120000_create_rsv_temporadahotel_tables.php` |
| Modelos | `app/Models/Modulos/Reservacion/TemporadaHotel.php`, `TemporadaHotelExclusion.php` |
| Regla | `app/Services/Reservaciones/VisibilidadHotelService.php` (modo `ocupacion`, `automatica`, paso de temporada, `avisoHotel()`, `reglaGeneral()`) |
| Pantalla | `DisponibilidadService::armar()` devuelve `avisoHotel`; `ReservacionController::disponibilidad` y `::catalogoInvitado` lo exponen |
| Receptor | `TemporadaHotelController` (`aplicar`, `estado`), `AplicarTemporadaHotelService`, `AplicarFormRequest`, `TemporadaHotelRechazadaException`, `routes/modulos/reservaciones/temporadaHotel.php` (cargado en `bootstrap/app.php`) |
| Config | `config/reservacion.php` (`automatica`, `temporadas`, `temporadas_cache_segundos`), `config/replicacion.php` (`temporadas.habilitada`) |
| Pruebas | `tests/Unit/Reservaciones/TemporadaHotelCalendarioTest.php`, `VisibilidadHotelServiceTest.php` (+17), `tests/Feature/Reservaciones/VisibilidadHotelTest.php` (+10), `TemporadaHotelReplicaTest.php`, `Concerns/MontaEsquemaTemporadaHotel.php` |

**Front del portal (CL032FRONT):** `src/components/alert/AvisoInformativo.vue` · `reservacionStore.js` (`avisoHotel`) · `DisponibilidadSocio.vue` (aviso en el bloque del Hotel 2) · `HabitacionesInvitado.vue` (aviso en el área principal) · `__tests__/AvisoTemporadaHotel.spec.js`

**Reservaciones V2 (CL034RESERVACIONESV2):**

| Tipo | Archivo |
|---|---|
| SQL | `database/sql/temporadas_hotel/01_rsv_temporadahotel.sql`, `02_seg_accionreservaciones.sql` (118 agregar, 119 eliminar, 120 sincronizar; la 117 es de otro módulo), `03_seg_ruta_menu.sql` (7 permisos + menú Catálogos → Temporadas Hotel 2), `99_reversion.sql` |
| Modelos | `app/Models/Modulos/Reservaciones/TemporadaHotel.php`, `TemporadaHotelExclusion.php`; `TipoHabitacion::inventarioHotel()` |
| Lógica | `app/Http/Controllers/Reservaciones/TemporadaHotelController.php` (`index`, `listar`, `guardar`, `eliminar`, `previsualizar`, `validar`, `sincronizar`), `app/Services/Reservaciones/TemporadaHotelService.php`, `app/Library/FSG/ReplicacionTemporadas/ReplicadorTemporadasPortal.php`, `config/replicaciontemporadas.php` |
| Validación | `app/Http/Requests/Reservaciones/TemporadaHotel/{Guardar,Eliminar,Listar,Previsualizar}FormRequest.php` + `NormalizaTemporada.php` |
| Resource | `app/Http/Resources/Reservaciones/TemporadaHotel/ListaResource.php` |
| Rutas | `routes/modulos/reservaciones/temporadaHotel.php` (incluido en `routes/web.php`) |
| Pantalla | `resources/views/modulos/reservaciones/temporada-hotel/index.blade.php` → `<reservaciones-temporada-hotel-index>`; `resources/js/modulos/reservaciones/temporada-hotel/Index.vue` + `components/{PruebaEstancia,SemanasExcluidas,VistaPrevia,ValidadorPortalModal}.vue`; `resources/js/stores/reservaciones/temporadaHotelStore.js`; registro en `resources/js/app.js` |
| Pruebas | `tests/Unit/TemporadaHotelCalendarioTest.php`, `tests/Feature/TemporadaHotelTest.php` |

Esquemas de referencia actualizados: `CL032RESERVACIONESENLINEA/asturias_api.sql` y `CL034RESERVACIONESV2/public/cl038reservacionesv2.sql`.

### 13.3 Despliegue (orden obligatorio)

1. **Portal**
   - Ejecutar `database/sql/rsv_temporadahotel.sql` (o `php artisan migrate`).
   - `.env`:
     ```dotenv
     RESERVACION_VISIBILIDAD_HOTEL_ACTIVA=true
     RESERVACION_VISIBILIDAD_HOTEL_AUTOMATICA=true      # false = solo temporadas
     RESERVACION_VISIBILIDAD_HOTEL_MODO=ocupacion
     RESERVACION_VISIBILIDAD_HOTEL_UMBRAL=150
     RESERVACION_VISIBILIDAD_HOTEL_TEMPORADAS=true
     REPLICACION_TEMPORADAS_HABILITADA=true
     RESERVACION_TIPOS_NO_VENTA_LINEA=9                  # tipos especiales que no se venden en línea
     ```
   - Desplegar el código y ejecutar `php artisan config:cache && php artisan route:cache`.
   - ⚠ `.env-pro` **no** se modificó. Hoy no define `RESERVACION_VISIBILIDAD_HOTEL_ACTIVA`, así que en producción la regla está apagada (se ven los dos hoteles). Encenderla es una decisión de liberación.
2. **Front del portal**: compilar y publicar.
3. **Reservaciones V2**
   - Ejecutar en orden `01`, `02` y `03` de `database/sql/temporadas_hotel/`. Antes, revisar en `03` los perfiles (`@perfiles_consulta` / `@perfiles_captura`).
   - `.env` (opcional): `REPLICACION_TEMPORADAS_ACTIVO=true`. URL y secreto caen en cascada a `REPLICACION_PORTAL_URL` / `REPLICACION_PORTAL_HMAC_SECRET`, que ya usa el canal de catálogos.
   - Desplegar el código, `npm run build`, `php artisan route:clear && php artisan config:clear`.
   - En la pantalla: **Validar con el portal**. Debe decir "Todo empatado".

**Rollback:**
- Fase 2: `RESERVACION_VISIBILIDAD_HOTEL_TEMPORADAS=false` en el portal (o eliminar las temporadas en V2).
- Fase 1: `RESERVACION_VISIBILIDAD_HOTEL_MODO=cantidad`, `UMBRAL=170`.
- V2: `99_reversion.sql`, solo después de dejar el portal sin temporadas.

### 13.4 Resultados de pruebas

| Proyecto | Suite | Resultado |
|---|---|---|
| Portal | Nuevas / ampliadas de temporadas y visibilidad | 23 calendario + 34 unitarias de visibilidad + 19 de integración + 13 del receptor: **todas pasan** |
| Portal | Tipos que no se venden en línea | 6 de integración (pantalla, regla apagada, lista con comas, alta, contadores, válvula) + verificación con datos reales: la lista de socio ya no trae el tipo 9 |
| Portal | Suite completa (`php artisan test`) | 542 pruebas, 536 pasan. Las **6 que fallan ya fallaban antes** de este cambio (acceso por tipo de membresía, bloqueo en login, `ExampleTest`, distribución automática de invitados); ninguna toca este módulo |
| Front | Vitest completo | 50 archivos, **655 pruebas, todas pasan** (10 nuevas del aviso) |
| V2 | Calendario (vector compartido) + integración del módulo | 23 + 24: **todas pasan** |
| V2 | Regresión `tests/Unit` + `tests/Feature` (sin los 5 archivos con `RefreshDatabase`) | 610 pruebas, 500 pasan. Las **110 que fallan ya estaban rotas** y no tocan este módulo: 87 usan la clase `App\Library\FSG\PaqueteService` (ya no existe), más métodos inexistentes en `SolicitudOperacion`, un `TypeError` en `Leyendas::calcularPrecioSocio` y `ExampleTest` |
| V2 | Compilación de la pantalla (`vite build`) | Correcta |
| ⚠ V2 | Suite completa (`vendor/bin/phpunit`) | **No correrla así.** `BeneficiarioFiscalModelTest`, `SolicitudDetalleModelTest`, `SolicitudModelTest`, `OpcionesArrayTest` y `OpcionesOrdenamientoTest` usan `RefreshDatabase` y **borran `cl038reservacionesv2_testing`** (`migrate:fresh` falla a la mitad). Ocurrió el 2026-10-02 y la base se restauró clonando `cl038reservacionesv2` |
| Punta a punta (local) | V2 → portal real con firma HMAC | Agregar publica y empata (mismo hash); el portal aplica los 6 ejemplos del cliente; eliminar publica y vacía la réplica; con el portal caído, 502 y nada guardado; bitácora de agregar y eliminar registrada (la prueba se hizo con la numeración anterior 117-119; las acciones se renumeraron a 118-120 el 2026-10-06) |
