# ARQUITECTURA DE SERVIDORES — Reservaciones en Línea (CL032)

> **Documento de referencia obligatorio.**
> Define **dónde vive cada dato, quién puede hablar con quién y cómo se calcula la disponibilidad**.
>
> Es complementario, **no sustituto**, de [ARQUITECTURA_PROYECTO.md](ARQUITECTURA_PROYECTO.md):
> - `ARQUITECTURA_PROYECTO.md` → **cómo se programa** (tablas, prefijos, naming, artefactos, UI Kit).
> - `ARQUITECTURA_SERVIDORES.md` (este) → **dónde vive el dato y qué está permitido a nivel de red**.
>
> Toda propuesta de solución en este proyecto **debe ser verificable contra este documento**. Una solución que viole una regla de red (§4) es un **bug de seguridad**, no una decisión de diseño.

---

## 1. Resumen ejecutivo

El sistema de **Reservaciones en Línea** expone al público la consulta de disponibilidad y la reserva de habitaciones. La disponibilidad **se calcula en el servidor privado de Reservaciones**, que es la única fuente de verdad porque ahí conviven las reservaciones en línea, las ventas mayores y las **ventas a mostrador del Hotel** (proyecto en migración). Cualquier cálculo que ignore alguna de esas tres fuentes produce **sobreventa**.

El reto central es: **dar disponibilidad en tiempo real al público sin que el público pueda alcanzar la base de datos privada.**

La solución es un **servidor intermedio** en la red privada, que funciona como **compuerta de caché**: `Reservaciones V2` (que vive junto al maestro) calcula la disponibilidad y la expone por una API interna; el intermedio la consume cada 10 s y la guarda en **caché (Redis)**; el público lee de esa caché por **ZeroTier**. Si el servidor público es comprometido, el atacante llega —como máximo— a una API que devuelve números de cupo, **nunca a una base de datos** ni a Reservaciones ni a Tlalpan.

> **Nota de diseño (14-jul-2026):** una versión previa de este documento planteaba **replicar 4 tablas** de MySQL al intermedio. Se descartó: una réplica nativa exige la credencial `REPLICATION SLAVE`, que lee el binlog **completo** (130 tablas), viviendo en el servidor que el público puede tocar. El modelo actual —API de solo lectura + caché, **sin base de datos en el intermedio**— elimina esa credencial. Ver [STACK_CL032APIDISPONIBILIDAD.md](STACK_CL032APIDISPONIBILIDAD.md) §0.

---

## 2. Inventario de servidores

| # | Servidor | Red | Base de datos | MySQL | Rol |
|---|----------|-----|---------------|-------|-----|
| 1 | **Tlalpan** | Privada | `siasturianov6` (~395 tablas) | 8.0.36 | Sistema central (SICAM v6): membresías, socios, castigos, citas, incidencias, cuotas, ausencias, beneficiarios |
| 2 | **Reservaciones** | Privada | `cl038reservacionesv2` (130 tablas) | 8.0.44 | **Fuente de verdad de la disponibilidad.** Reservaciones presenciales + ventas mayores + Hotel/mostrador (en migración) |
| 3 | **Intermedio** | Privada, alcanzable por ZeroTier | **Ninguna** (solo Redis) | — | **Compuerta de caché** de disponibilidad. Consume la API de Reservaciones V2 y la sirve al público. **Aislante de seguridad.** Sin BD. |
| 4 | **Público** | Internet (hoy GoDaddy) | `asturias_api` (39 tablas) | Se actualizará a ≥ 8 | Portal público: reservaciones en línea, consulta de estado de cuenta, bloqueos |

> **Servidor 3 es nuevo.** No existe hoy. Es la pieza que habilita el tiempo real sin exponer las bases privadas.

---

## 3. Topología y flujo de datos

```
═══════════════ RED PRIVADA (sin acceso desde Internet) ═══════════════

  1. TLALPAN  ·  siasturianov6 (SICAM v6)
       │
       │  push (outbox sinc_cola): castigos, citas, incidencias,
       │  cuotas, ausencias, beneficiarios, membresías
       ▼
  2. RESERVACIONES  ·  cl038reservacionesv2   ◄── FUENTE DE VERDAD · BD maestra
       │                                          (reservaciones + ventas mayores + Hotel/mostrador)
       │
       │   ┌─ Reservaciones V2 (vive junto al maestro) ─────────────────┐
       └──►│  Calcula el cupo desde las tablas rsv_* y lo expone en:     │
           │  GET /interno/disponibilidad  → (idTipoHabitacion,fecha,cupo)│
           └───────────────────────────────┬────────────────────────────┘
                                            │  Servicio 1: jala cada 10 s
                                            ▼
  3. INTERMEDIO  ·  CL032APIDISPONIBILIDAD  ·  Redis (caché) — SIN base de datos
       - Guarda el snapshot + sello de frescura.  NO calcula.  Solo GET.
       - Si la caché envejece (upstream caído) → responde "no disponible".
                                            │
═══════════ ZeroTier (VPN cifrada) ═════════╪══ el público SOLO alcanza el Intermedio ══
            NUNCA Tlalpan ni Reservaciones  │   Servicio 2: jala cada 10 s
                                            ▼
═══════════════════════ INTERNET (público) · asturias_api ═════════════════════

  4. CL032BACK (Laravel + Redis caché)  ◄──►  CL032FRONT (Vue SPA)  ──►  Socio
       │
       │  el socio lee la caché de borde (<1 ms).  Nunca toca el maestro.
       │
       └─ RESERVA ─► cola ─► Reservaciones V2 intenta el HOLD en transacción (lockForUpdate)
                            ▲ ESTA es la ruta que evita la sobreventa (§8),
                              NO la cadena de caché de arriba (que es solo para mostrar).
```

### Dirección del flujo (regla mnemotécnica)

- **Los datos bajan por jale (pull) periódico cada 10 s**: Reservaciones V2 (calcula) → Intermedio (caché) → Público (caché). Cada salto es un refresco de caché, no una llamada en vivo del usuario.
- **El usuario lee la caché más cercana** (la del público). **Nunca** dispara una cadena síncrona hasta el maestro.
- **Las reservas confirmadas suben por la cola**, nunca por conexión directa (§7). Esta —y solo esta— es la ruta que evita la sobreventa (§8).

---

## 4. Reglas de red — INVIOLABLES

Estas reglas existen porque el servidor público es la única superficie expuesta a Internet. Su compromiso es un escenario que se **asume**, no que se descarta.

| # | Regla | Razón |
|---|-------|-------|
| **R1** | El servidor **público NUNCA** abre conexión a **Tlalpan** ni a **Reservaciones**. Ni a MySQL, ni a HTTP, ni por VPN. | Si el público cae, el atacante no debe tener ninguna ruta hacia las bases maestras. |
| **R2** | El servidor **público SOLO** puede alcanzar el **Intermedio**, y **solo por ZeroTier**. | Un único punto de contacto, cifrado, autenticado y auditable. |
| **R3** | El público consume al Intermedio con **API key + lista blanca de IP**. El Intermedio **no tiene base de datos**: no hay credencial de BD que robar. | Credencial robada = leer números de cupo por una API GET. Nada más. |
| **R4** | El Intermedio **jamás escribe** hacia el mundo privado. Solo **lee** de la API de Reservaciones V2 (GET). | El Intermedio es territorio semi-hostil: es lo que el público toca. |
| **R5** | El Intermedio **no contiene ni un solo dato personal**. Nada de socios, membresías, beneficiarios, castigos, correos, teléfonos ni importes de cuotas. Solo cupos, fechas y cantidades. | Una fuga en el Intermedio no debe ser una fuga de datos personales (implicación legal, no solo técnica). |
| **R6** | Toda escritura del público hacia el mundo privado (una reserva confirmada) viaja por **cola asíncrona con idempotencia**, nunca por conexión directa a la base privada. | La escritura es el vector más peligroso. Se valida del lado privado, siempre. |
| **R7** | El servidor **Tlalpan nunca habla directamente con el público**. Su información viaja a través del pipeline de sincronización. | Reduce a uno el número de servidores privados con exposición indirecta. |

> **Cómo aplicar estas reglas a una propuesta:** si una solución requiere que el código en `CL032BACK` (público) haga `DB::connection('reservaciones')` o llame a un endpoint alojado en Reservaciones o Tlalpan → **la propuesta es inválida**, sin importar qué tan conveniente sea.

---

## 5. Proyectos y su ubicación

| Proyecto | Servidor | Stack | Responsabilidad |
|----------|----------|-------|-----------------|
| **`CL032BACK`** | 4. Público | Laravel (API) | API del portal: sesión del socio, catálogos, cotización, creación de reserva, pagos. Consume disponibilidad **del Intermedio** (desde su propia caché). |
| **`CL032FRONT`** | 4. Público | Vue (SPA) | Interfaz pública. Consume **exclusivamente** a `CL032BACK`. Nunca al Intermedio directamente. |
| **`CL032APIDISPONIBILIDAD`** | 3. Intermedio | Laravel + Redis (sin BD) | **Un solo trabajo**: cachear la disponibilidad que calcula Reservaciones V2 y servirla; si no puede refrescarla, degradar con un mensaje claro. No calcula, no tiene BD, no datos personales. |
| **`Reservaciones V2`** | 2. Reservaciones | Laravel (existente) | **Calcula** la disponibilidad desde las tablas `rsv_*` y la expone por una API interna de solo lectura que el Intermedio consume. |

### Sobre la nomenclatura

Se respeta la convención de la casa (`CL0XX` + descriptor). Nota: el nombre `CL0032BACKRVENLINEA` (4 dígitos) fue **corregido a `CL032BACK`** (3 dígitos) para ser consistente con la carpeta contenedora `CL032RESERVACIONESENLINEA` y con el resto del portafolio (`Reservaciones V2`, `CL001SICAMV2`).

### Regla de acoplamiento

Back y Front se desarrollan y despliegan **por separado**. El contrato entre ambos es **el JSON de la API**, no el código compartido. `CL032FRONT` no asume nada sobre la base de datos: consume recursos (`Resources`) de Laravel.

---

## 6. Modelo de disponibilidad (LA REGLA MÁS IMPORTANTE)

### 6.1 Cómo se calcula realmente

La disponibilidad **NO es por habitación física**. Es por **cupo de tipo de habitación** (*allotment*). `hot_habitacion` (la habitación física) solo se asigna en el **check-in** y **no interviene** en el cálculo de disponibilidad de una reserva.

Por cada `idTipoHabitacion` y por **cada día** del rango solicitado:

```
disponible(tipo, día) =
      rsv_tipohabitacion.tipoHabitacionCantidad                       -- inventario total del tipo
    − Σ rsv_reservacionhabitacion.reservacionHabitacionCantidad       -- ocupado
        WHERE reservacionHabitacionEstado IN ('A','T')
          AND día ∈ [reservacionHabitacionFechaLlegada, reservacionHabitacionFechaSalida)
    − Σ rsv_bloqueo.bloqueoCantidad                                   -- cupo retirado
        WHERE bloqueoEstado = 'A'
          AND día ∈ [bloqueoFechaInicio, bloqueoFechaFin]
```

Se filtra además por `rsv_reservacion.reservacionEstado` (la cabecera).

> **El estado `'T'` (Temporal) SÍ consume inventario.** Es un *hold*: aparta el cupo hasta que expira. Ignorarlo en el cálculo **produce sobreventa directa**. Esta es la trampa más fácil de caer.

**Convención de fechas**: la noche de salida **no se ocupa**. El rango es semiabierto `[llegada, salida)` — una reserva del 10 al 12 ocupa las noches del 10 y del 11.

### 6.2 Las 4 tablas del cálculo

`Reservaciones V2` calcula la disponibilidad con estas —y solo estas— tablas. El resultado (no las tablas) viaja al Intermedio:

| Tabla | Campo clave | Papel |
|-------|-------------|-------|
| `rsv_tipohabitacion` | `tipoHabitacionCantidad` | **Inventario** (cupo total por tipo) |
| `rsv_reservacionhabitacion` | `reservacionHabitacionCantidad`, `Estado` (`A`/`C`/`T`), `FechaLlegada`, `FechaSalida` | **Ocupación** |
| `rsv_bloqueo` | `bloqueoCantidad`, `bloqueoEstado` (`A`/`E`), `FechaInicio`, `FechaFin` | **Cupo retirado** (mantenimiento, eventos) |
| `rsv_reservacion` | `reservacionEstado` (`T`/`C`/`F`/`R`/`H`) | **Cabecera** (filtro de estado) |

Los índices en Reservaciones confirman este diseño: `idx_rh_disponibilidad`, `idx_bloqueo_disponibilidad`, `idx_rsv_join_disponibilidad`. **Úsalos en la consulta agregada de Reservaciones V2.**

> **Lo que sale de la red privada son números, no tablas:** `(idTipoHabitacion, fecha, cupoLibre[, precio])`. Ese es el valor del Intermedio. Ninguna fila contiene un dato personal.

### 6.3 Tarifas y "temporadas"

**No existe tabla `temporada`.** La estacionalidad se modela con paquetes vigentes por fechas:

- `rsv_paquete` — **es la temporada de facto**: `paqueteFechaInicio/Fin`, `paqueteDiaInicio/Fin`, `paqueteDiasMinimo/Maximo` (estancia mínima/máxima), `paqueteBloqueo`.
- `rsv_tipohabitacionprecio` — precio por tipo, por perfil (`Socio`/`Invitado`) y **por año** (`tipoHabitacionPrecioAnioAplicacion`). UNIQUE (tipo, perfil, año).
- `rsv_tipohabitacionpaquete` — importe del paquete por tipo y año.
- `rsv_extras` / `rsv_reservacionhabitacionextra` — cargos adicionales por día y tipo de persona.

El campo `reservacionHabitacionTipo` (`I`/`S`) decide qué tarifa aplica.

> **Decisión pendiente:** el precio puede calcularlo **Reservaciones V2** (e incluirlo en el snapshot, junto a `cupoLibre`) o el Público. Recomendación: **en Reservaciones V2**, porque la regla de estancia mínima (`paqueteDiasMinimo`) es parte de *"¿puedo reservar esto?"*, no de *"¿cuánto cuesta?"*. Si se decide así, el snapshot incluye `precio` por `(tipo, fecha)` — sigue sin datos personales.

### 6.4 Lo que NO se debe hacer

- ❌ **No usar `hot_tipohabitacion`** para disponibilidad. Es un catálogo paralelo, **no tiene campo de cantidad**, y su enum de hotel (`'A'`/`'N'`) **no coincide** con el de `rsv_tipohabitacion` (`'V'`/`'N'`).
- ❌ **No usar `hot_bloqueo`**. Es bloqueo de una habitación *física* por daño/mantenimiento, distinto de `rsv_bloqueo` (cupo).
- ❌ **No revivir `wd_tipohabitacion.tipoHabitacionNoHabitacionesDisponibles`** (ver §9).

---

## 7. Sincronización

### 7.1 Lo que YA existe (y hay que aprovechar)

Tlalpan ya tiene implementado un **patrón outbox**, recién estrenado (`AUTO_INCREMENT = 9`):

- **`sinc_cola`** — outbox genérico: `colaTabla`, `idRegistro`, `colaAccion` (`INSERT`/`UPDATE`/`DELETE`), `colaPayload` (JSON), `colaEstado` (`PENDIENTE` → `EN_COLA` → `ENVIADO` → `ERROR`), `colaIntentos`, timestamps de creación/encolado/procesado.
- **`sinc_vigilancia`** — heartbeat por tabla.

**El camino al tiempo real es completar este mecanismo, no inventar uno nuevo.**

### 7.2 Lo que FALTA (trabajo a realizar)

1. **Inbox del lado público.** `asturias_api` **no tiene** tabla de recepción ni de idempotencia. Sin inbox, un reintento de la cola duplica registros. (Reservaciones sí tiene el patrón resuelto: `hot_checkinidempotencia` — replicarlo.)
2. **Nadie puede sincronizar incrementalmente hoy.** Ninguna tabla de negocio de `asturias_api` tiene `updated_at`, `sync_at`, versión ni hash de fila (el único `updated_at` es el de `users` de Laravel). Por eso la sincronización actual es *truncate + recarga* o por llave natural (`membresiaNo`). **Toda tabla espejo nueva debe nacer con `updated_at` y puntero al ID de origen.**
3. ~~**Resolver la ambigüedad de origen.**~~ **RESUELTO (12-ago-2026).** `wd_solicitud.idSolicitudSicam` referencia `inv_solicitud`, tabla que existe en ambas bases privadas. **El origen canónico de una solicitud de invitados es Tlalpan**, porque ahí —y solo ahí— se califica, y desde ahí sale el fan-out a Campestre y a Reservaciones. Reservaciones y Campestre son réplicas de lectura para operación. Ver [PROPUESTA_TRANSFERENCIA_SOLICITUDES_INVITADOS.md](PROPUESTA_TRANSFERENCIA_SOLICITUDES_INVITADOS.md).

### 7.3 Datos que viajan de privado a público

| Dato | Origen | Destino público | Estado hoy |
|------|--------|-----------------|-----------|
| Castigos, citas, incidencias | `tr_castigo`, `tr_cita`, `tr_incidencia` (Tlalpan) | **`wd_bloqueo`** | ✅ Buen diseño: no replica las tablas, replica **el efecto** (¿bloqueado sí/no?) con puntero al origen (`idCastigoSicam`, `idCitaSicam`, `idIncidenciaSicam`). **Conservar este patrón.** |
| Membresías | `soc_membresia` | `wd_membresia` | ✅ **Sí tiene puntero de origen: `wd_membresia.idIdentificador` ES el `soc_membresia.idMembresia` de Tlalpan.** (Corregido 12-ago-2026; antes se documentaba como ausente.) ⚠️ Sigue sin `updated_at`, y los beneficiarios están aplanados en `mediumtext`. |
| Beneficiarios | `soc_beneficiario` | *(no existe tabla)* | ❌ Viven desnormalizados dentro de `wd_membresia` y `wd_datossocios` |
| Cuotas / movimientos | `caj_movimientosencuenta`, `caj_recibo` | `wd_movimientos`, `wd_recibo`, `wd_factura` | ⚠️ Enlazan por `idMembresia`, sin puntero de origen |
| **Ausencias** | `tr_ausencia` | *(no existe)* | ❌ **No se replican.** Si la regla de negocio requiere ausencias, hay que crearlo. |
| Reservaciones (eco) | `rsv_reservacion` | `wd_reservacion` | ✅ Tiene `idReservacionRemoto` |

> **Regla de oro para el bloqueo del socio:** el público **no** debe reimplementar la lógica de "¿este socio puede reservar?". Recibe el **veredicto** ya calculado (`wd_bloqueo`), no los insumos. Es el patrón que ya funciona y hay que extenderlo.

---

## 8. Escritura: cómo se confirma una reserva

Una reserva creada en el portal público debe llegar a Reservaciones **sin que el público escriba en la base privada** (R6):

1. Público valida al socio contra `wd_bloqueo` (local, ya sincronizado).
2. Público lee disponibilidad de **su propia caché** (alimentada por el Intermedio). Es informativa; puede estar desactualizada.
3. Público crea la reserva **en su propia base** (`asturias_api`) en estado *pendiente de confirmación*, con una **clave de idempotencia** única.
4. La reserva se **encola** hacia el mundo privado.
5. **Reservaciones consume la cola**, **revalida disponibilidad contra su propia base** (fuente de verdad) y confirma o rechaza.
6. El veredicto regresa al público, que actualiza el estado de la reserva.

> **El paso 5 no es opcional.** Entre la lectura del paso 2 y la confirmación del paso 5 pudo entrar una venta a mostrador del Hotel — y con la cadena de caché de 10 s, el dato del paso 2 tiene hasta ~20 s de antigüedad. **La disponibilidad que ve el público siempre es una foto del pasado**; solo Reservaciones puede autorizar, y lo hace tomando el *hold* dentro de una transacción con `lockForUpdate` (recalculando el cupo con el candado puesto). Diseñar el flujo asumiendo que el paso 2 puede estar equivocado es lo que evita la sobreventa.

Si se requiere garantía dura para el usuario final, el paso 3 debe generar un **hold** (estado `'T'`) en Reservaciones con expiración, que consume inventario mientras el socio completa el pago.

---

## 9. Deuda técnica y riesgos conocidos

Documentados aquí porque **afectan las decisiones de diseño**, no como reproche.

| # | Riesgo | Impacto | Acción |
|---|--------|---------|--------|
| **D1** | `wd_tipohabitacion.tipoHabitacionNoHabitacionesDisponibles` es un **caché de disponibilidad sin dimensión de fecha**. | Es **estructuralmente incapaz** de responder "¿hay cupo el 14 de agosto?". | **Eliminar el campo**, no migrarlo. Reemplazado por la caché con fecha (`idTipoHabitacion, fecha, cupoLibre`). |
| **D2** | `rsv_reservacionhabitacion` **no tiene FK declaradas** hacia `rsv_reservacion` ni `rsv_tipohabitacion` (solo índices). | Registros huérfanos pueden inflar o desinflar el cupo silenciosamente. | Auditar huérfanos en Reservaciones y declarar las FK en el maestro. Afecta directamente el cálculo de Reservaciones V2. |
| **D3** | **Tablas MyISAM** en el flujo de reserva (`rsv_reservacionhabitacionpaquete`, `rsv_solicitudoperacion`) mientras el resto es InnoDB. | **Sin transacciones**: una reserva no es atómica de extremo a extremo. Con sobreventa de por medio, es crítico. | Migrar a InnoDB. |
| **D4** | Catálogos duplicados `rsv_tipohabitacion` vs `hot_tipohabitacion`, con **enums de hotel incompatibles** (`V`/`N` vs `A`/`N`). | Confusión garantizada al unir reservaciones con hotel. | Definir el catálogo canónico durante la migración de Hotel. |
| **D5** | `asturias_api` usa sintaxis antigua (`int(11)`) y **ninguna tabla de negocio tiene `updated_at`**. | Imposibilita el sync incremental. | Cubrir en la actualización a MySQL ≥ 8. |
| **D6** | Ninguna de las 3 bases usa UUID. Todo es `int AUTO_INCREMENT`. | Toda integración cross-servidor depende de enteros + columnas espejo (`idXxxSicam`, `idXxxRemoto`). | **Mantener el patrón de columna espejo.** No introducir UUID a medias. |

---

## 10. Checklist antes de proponer o programar cualquier cosa

Toda solución en este proyecto debe pasar estas preguntas:

1. **¿Toca disponibilidad?** → Entonces usa `rsv_tipohabitacion` + `rsv_reservacionhabitacion` + `rsv_bloqueo` + `rsv_reservacion`. Nada de `hot_*`. Y **cuenta el estado `'T'`**.
2. **¿El código público abre conexión a Tlalpan o Reservaciones?** → **Inválido** (R1).
3. **¿El código público escribe en una base privada?** → **Inválido** (R6). Va por cola con idempotencia.
4. **¿El snapshot que sale al Intermedio lleva algo con datos personales?** → **Inválido** (R5). Solo `(tipo, fecha, cupo[, precio])`.
5. **¿El público recalcula si un socio está bloqueado?** → **No.** Consume el veredicto de `wd_bloqueo`.
6. **¿La respuesta de disponibilidad se trata como definitiva?** → **No.** Solo Reservaciones autoriza (§8, paso 5).
7. **¿El campo existe literal en el SQL?** → Si no, **no se inventa** (ver `ARQUITECTURA_PROYECTO.md` §1).
8. **¿La tabla espejo nueva tiene `updated_at` y puntero al ID de origen?** → Si no, **no nace** (§7.2).

---

## 11. Glosario

| Término | Significado en este sistema |
|---------|---------------------------|
| **Disponibilidad** | Cupo libre **por tipo de habitación y por día**. Nunca por habitación física. |
| **Hold / Temporal (`'T'`)** | Reserva que **aparta cupo** sin estar confirmada. **Consume inventario.** |
| **Bloqueo (`rsv_bloqueo`)** | Cupo **retirado del inventario** (mantenimiento, evento). No confundir con `wd_bloqueo`. |
| **Bloqueo (`wd_bloqueo`)** | **Socio impedido de reservar** por castigo, cita o incidencia. No confundir con `rsv_bloqueo`. |
| **Intermedio** | Servidor privado (`CL032APIDISPONIBILIDAD`) alcanzable por ZeroTier desde el público. Compuerta de caché, sin base de datos. Aislante de seguridad. (En versiones previas se le llamó "Puente".) |
| **Fuente de verdad** | **Reservaciones (`cl038reservacionesv2`)**. Siempre. Todo lo demás es una copia con retraso. |
| **Sobreventa** | Vender una habitación que ya no existe. **El fallo que toda esta arquitectura existe para prevenir.** |

---

## 12. Pendientes de definición

- [ ] Confirmar si el **cálculo de tarifa** vive en Reservaciones V2 (dentro del snapshot) o en el Público (§6.3).
- [ ] Definir el **intervalo de refresco** de los dos servicios de caché (default 10 s) y el **umbral de degradación** (default 60 s). Ver [STACK_CL032APIDISPONIBILIDAD.md](STACK_CL032APIDISPONIBILIDAD.md) §4.
- [ ] Especificar el **contrato de la API interna** `GET /interno/disponibilidad` que expone Reservaciones V2 (auth, IP allowlist, formato).
- [ ] Definir si Reservaciones V2 **recalcula todo el snapshot cada 10 s** o mantiene su propia caché y recalcula solo las fechas afectadas por cada reserva.
- [ ] Definir la **política de expiración del hold** (`'T'`) para reservas en línea sin pagar.
- [ ] Definir si las **ausencias** (`tr_ausencia`) deben replicarse al público (hoy no lo hacen).
- [ ] Resolver la **ambigüedad de origen** de `wd_solicitud.idSolicitudSicam` (§7.2).
- [ ] Confirmar la **versión destino de MySQL** del servidor público (≥ 8) y el proveedor (GoDaddy no permite ZeroTier en hosting compartido → **probablemente requiere VPS**).
