# 08 · Desactivación y reversa

> **ESTADO ACTUAL: el esquema del canal inverso ESTÁ APLICADO.** Este documento describe cómo
> retirarlo si hiciera falta, y qué NO se puede tocar nunca. Lo único apagado hoy es la
> *recepción* (`REPLICACION_INVERSA_HABILITADA=false`); los punteros se siguen escribiendo.

---

## 8.1 Qué se hizo, en dos pasos

```bash
# 1 · Apagar la aplicación PRIMERO — LOS DOS interruptores
#     .env → REPLICACION_INVERSA_HABILITADA=false   (recepción)
#     .env → REPLICACION_INVERSA_PUNTEROS=false     (⚠ este es el que rompe la venta)
php artisan config:clear

# 2 · Retirar el esquema DESPUÉS
mysql -u USUARIO -p asturias_api < docs/replicacion/sql/desactivar_replica_inversa.sql
```

### ⚠️ El orden importa, y al revés rompe la venta

`MapeoReplicaService` se invoca desde `finalizarConfirmacion()`, que es el commit que
**asienta la venta** cuando llega el veredicto de Reservaciones V2. **Reservaciones V2 sigue enviando el `mapeo`
aunque el portal no lo use.**

Si las columnas desaparecen y **`REPLICACION_INVERSA_PUNTEROS` sigue en `true`**, ese commit
revienta **en cada venta confirmada**: el socio queda cobrado y su reservación atascada en
`Confirmando`, que es exactamente el escenario que la alerta `R1` existe para detectar.

⚠️ Es `punteros` el que hay que apagar, **no** `habilitada`. Apagar solo la recepción no basta:
el `mapeo` llega por el canal de IDA, que sigue vivo.

Por eso el SQL de desactivación lleva la comprobación previa en mayúsculas.

---

## 8.2 Qué se retiró

| Objeto | Tabla |
|---|---|
| `reservacionVersionReplica`, `reservacionHashReplica`, `reservacionFechaReplica` | `rsv_reservacion` |
| `idReservacionHabitacionPrivado` + `uq_rh_privado` | `rsv_reservacionhabitacion` |
| `idReservacionHabitacionPaquetePrivado` + `uq_rhp_privado` | `rsv_reservacionhabitacionpaquete` |
| `idReservacionHabitacionExtraPrivado` + `uq_rhe_privado` | `rsv_reservacionhabitacionextra` |

Más las dos filas de control en `migrations`, para que `migrate:status` diga la verdad: su
esquema ya no está aplicado. Si se dejaran, `php artisan migrate` las daría por hechas y al
reactivar no las volvería a correr.

**Antes de retirar se respaldó** a `bkp_replica_inversa_punteros` (cabecera + punteros de
habitación). Ocupa poco y evita que, al reactivar, cada reservación caiga al camino de respaldo
con su incidencia `R12`.

---

## 8.3 Qué NO se retiró, y por qué

### 🔴 `reservacionMaestro` y `reservacionFechaMaestro` — no se pueden quitar

`sp_rsv_marcar_replicacion` **lee** `reservacionMaestro` en cada disparo de los 12 triggers. Si
la columna desaparece, ese `SELECT` falla dentro de un trigger, y un error en un trigger
**aborta la sentencia de negocio**: el portal deja de poder insertar, actualizar o borrar en
las cuatro tablas de la reservación. **El socio no puede capturar.**

Y aunque no rompiera nada, sigue haciendo falta: es lo que impide que el portal destruya lo que
el club edita en Reservaciones V2, **y ese riesgo existe hoy**, con el canal inverso apagado. La maestría
no es del canal inverso; es del canal de ida.

### `idReservacionPrivado` y `reservacionNoPrivado`

Los escribe `sellarMaestriaPrivada()` con lo que trae el veredicto **del canal de ida**.
Quitarlos obligaría a tocar código que hoy funciona —y a decidir qué hacer con `folioVisible()`
y la decisión D1— sin ganar nada. Se quedan.

### `rsv_bitacorareplicacion` y `MonitorReservaciones`

Son la observabilidad **del canal de ida**: `R1` (venta esperando veredicto, el hueco más caro
del sistema), `R3` (fallo del canal de escritura), `R6`/`R15` (rezago y leases del outbox).
Quitarlas dejaría ciego justo el canal que sigue vivo.

### Todo el código del canal inverso

Controlador, FormRequest, servicios, métodos de modelo, `HmacSigner::*ConCuerpo`, el parámetro
`$modo` del middleware y las 34 pruebas. **Borrarlo habría significado rehacerlo.** Está inerte
detrás del interruptor y cubierto por pruebas que siguen corriendo en cada suite.

---

## 8.4 Cómo queda el sistema

| | Estado |
|---|---|
| Canal de ida (portal → Reservaciones V2) | 🟢 **Idéntico a antes** |
| Frontera de maestría | 🟢 **Activa** — sigue protegiendo los datos del club |
| Alertas `R1`, `R3`, `R6`, `R15` | 🟢 Activas |
| `/entrante` y `/estado` | 🔴 `404` — no registradas |
| `mapeo` del veredicto | ⚪ Llega y se ignora |
| Alertas `R9`, `R12`, `R13` | ⚪ Inertes: no hay canal que las dispare |

**Reservaciones V2 no requiere ningún cambio.** Puede seguir enviando `mapeo` y `reservacionNoRemoto` en
el veredicto; el portal los ignora sin error. Lo único que **no** debe hacer es encender su
consumidor `REPLICACION_PORTAL_HABILITADA`, porque recibiría `404` y descartaría las marcas.

---

## 8.5 Verificación de que la desactivación no rompió nada

```sql
-- 1 · Las columnas del canal inverso ya no están (0 filas cada una)
SHOW COLUMNS FROM `rsv_reservacion`                  LIKE 'reservacion%Replica';
SHOW COLUMNS FROM `rsv_reservacionhabitacion`        LIKE '%Privado';
SHOW COLUMNS FROM `rsv_reservacionhabitacionextra`   LIKE '%Privado';

-- 2 · LA FRONTERA SIGUE EN PIE (1 fila cada una — CRÍTICO)
SHOW COLUMNS FROM `rsv_reservacion` LIKE 'reservacionMaestro';
SHOW COLUMNS FROM `rsv_reservacion` LIKE 'reservacionFechaMaestro';

-- 3 · El canal de ida, completo
SELECT COUNT(*) AS debe_ser_12 FROM information_schema.TRIGGERS
 WHERE TRIGGER_SCHEMA = DATABASE() AND EVENT_OBJECT_TABLE LIKE 'rsv_reservacion%';
SHOW PROCEDURE STATUS WHERE Db = DATABASE() AND Name = 'sp_rsv_marcar_replicacion';

-- 4 · PRUEBA FUNCIONAL: una Temporal SIGUE marcando
SET @idt := (SELECT idReservacion FROM rsv_reservacion
              WHERE reservacionEstado = 'Temporal' AND reservacionMaestro = 'Portal'
              ORDER BY idReservacion DESC LIMIT 1);
UPDATE rsv_reservacion SET reservacionNotas = CONCAT(IFNULL(reservacionNotas,''),' ')
 WHERE idReservacion = @idt;
SELECT COUNT(*) AS debe_ser_1 FROM rsv_replicacion
 WHERE idReservacion = @idt AND replicacionEstado = 'Pendiente';

-- 5 · Y una Reservado NO marca: la frontera sigue cortando
SET @idr := (SELECT idReservacion FROM rsv_reservacion
              WHERE reservacionMaestro = 'Privado' ORDER BY idReservacion DESC LIMIT 1);
SELECT COUNT(*) INTO @antes FROM rsv_replicacion WHERE idReservacion = @idr;
UPDATE rsv_reservacion SET reservacionNotas = CONCAT(IFNULL(reservacionNotas,''),' ')
 WHERE idReservacion = @idr;
SELECT COUNT(*) = @antes AS debe_ser_1 FROM rsv_replicacion WHERE idReservacion = @idr;
```

Y en la aplicación:

```bash
php artisan route:list --path=replicacion     # solo 2 rutas: pendientes y veredicto
php artisan test                              # la suite completa sigue verde
```

> Las suites del canal inverso (`MapeoReplicaTest`, `ReplicaInversaTest`) **siguen corriendo**:
> encienden el interruptor en su `setUp()` y crean su propio esquema en SQLite. Documentan cómo
> debe comportarse el canal cuando esté activo, y protegen ese código de romperse mientras
> duerme.

---

## 8.6 Reversa de la reversa

Volver a encenderlo es [07 · Activación paso a paso](07-ACTIVACION-PASO-A-PASO.md). En corto:

```bash
mysql -u USUARIO -p asturias_api < docs/replicacion/sql/activar_replica_inversa.sql
# .env → REPLICACION_INVERSA_HABILITADA=true
php artisan config:clear
```

Y **después** encender Reservaciones V2 — nunca antes.

---

**Anterior:** [07 · Activación](07-ACTIVACION-PASO-A-PASO.md) ·
**Siguiente:** [09 · Observabilidad](09-OBSERVABILIDAD.md)
