# PROMPT — Purga de solicitudes y documentos vencidos · lado PORTAL (`CL032BACK`)

> **Ejecutar en:** `D:\FOURSIDES\CL001ASTU\PRE-PRODUCCION\VILLASTUR\CL032RESERVACIONESENLINEA\CL032BACK`
> **Referencias:** `NOTA_COORDINACION_DOCUMENTOS_TLALPAN.md` §5 · `PROPUESTA_DOCUMENTO_IDENTIFICACION_INVITADOS.md` (D6, purga no autorizada) · `CONFIGURACION_DOCUMENTOS_PORTAL.md` §5

Actúa como desarrollador senior de este proyecto. Vas a implementar la purga que hoy **no existe** y que la documentación viene señalando como pendiente: el Portal conserva indefinidamente identificaciones oficiales de terceros.

**El principio:** el Portal es un **punto de captura**, no un archivo histórico. El histórico vive en los servidores principales —Tlalpan para las solicitudes de invitados, Reservaciones V2 para las reservaciones—. Una vez terminada la estancia, el Portal no tiene motivo para conservar los datos personales.

---

## Lo que hay que entender antes de escribir una línea

Investigué el código. **Hay cuatro hallazgos que cambian el diseño de la purga.** Verifícalos tú mismo; no los tomes por buenos porque estén escritos aquí.

### Hallazgo 1 — Purgar EL MISMO DÍA de la salida genera ruido durante 24 h

`CL001SICAMV2/app/Library/FSG/InvitadosPortal/TransferenciaEstadosPortal.php:37` filtra así lo que Tlalpan reenvía cada ciclo:

```php
->whereDate('solicitudFechaSalida', '>=', now()->toDateString())
```

Es decir: **Tlalpan sigue enviando el estado de una solicitud durante todo el día de su fecha de salida**, y deja de hacerlo al día siguiente.

Del lado del Portal, `TransferenciaController::estado()` responde a un veredicto sobre una solicitud que ya no existe con `Log::warning('veredicto sobre una solicitud inexistente')`. Tlalpan corre ese ciclo **cada 5 minutos**.

> **Purgar cuando `solicitudFechaSalida = hoy` produciría ~288 advertencias por solicitud purgada, durante ese día.** No rompe nada —el flujo está diseñado para tolerarlo— pero llena el log de ruido que oculta incidencias reales.
>
> **Purgar cuando `solicitudFechaSalida < hoy` se alinea exactamente con el filtro de Tlalpan y produce cero advertencias.**
>
> El negocio pidió "el mismo día de la salida". **La diferencia es de un día y elimina el problema por completo.** Impleméntalo como `< hoy` (con un margen configurable, ver más abajo) y deja constancia del motivo. Si el negocio insiste en el mismo día, la vía correcta es coordinar el filtro de Tlalpan, no absorber el ruido.

### Hallazgo 2 — Purgar una solicitud NO transferida destruye el histórico

La premisa "el histórico se queda en los servidores principales" **solo se cumple si el dato llegó allá**. Una solicitud del Portal existe en Tlalpan únicamente cuando tiene `idSolicitudTlalpan` (lo asigna `Solicitud::marcarTransferida()` desde el acuse `/recibidas`).

`TransferenciaController::esTransferible()` ya documenta el caso en que **no** llega: una membresía sin `idIdentificador` se aparta del lote y se queda muda hasta que alguien corrija el padrón. Esa solicitud **nunca llegó a Tlalpan**.

> **Purgarla sería una pérdida de datos definitiva, no una purga.** El filtro debe exigir que la transferencia esté consumada, y las que no lo estén tienen que quedar **retenidas y reportadas**, no borradas ni ignoradas en silencio.

### Hallazgo 3 — La reservación tiene un outbox que puede estar abierto

`rsv_reservacion.idSolicitud` vincula la reservación con la solicitud de invitados (migración `2026_07_17_120000_add_idsolicitud_to_rsv_reservacion.php`).

Pero la reservación se replica a Reservaciones V2 por el outbox `rsv_replicacion`, cuyo modelo define `ESTADOS_ABIERTOS = ['Pendiente', 'Error']`.

> **Borrar una reservación con una marca abierta en `rsv_replicacion` significa que esa escritura nunca llega al maestro.** Mismo problema del hallazgo 2, en el otro canal.

Además, `Reservacion::ESTADOS_EN_VUELO = ['Temporal', 'Confirmando']`. Una reservación en `Confirmando` es **dinero en vuelo**: el socio pagó y el veredicto de Reservaciones V2 no ha llegado. `ReservacionesAlertarConfirmando` existe justo porque ahí cancelar automáticamente es peor que esperar.

> **Nunca purgar una reservación en vuelo, sin importar su fecha.**

### Hallazgo 4 — El borrado del disco resuelve por nombre sin extensión

Las identificaciones viven en `storage/app/private/solicitudinvitado/{idSolicitud}/{idInvitado}.{ext}`, y con la admisión de PDF la extensión ya no es predecible.

`InvitadoController::eliminar()` (~línea 248) ya resuelve esto **listando el directorio y comparando el nombre sin extensión**. Y `SolicitudController` (~línea 255) borra el **directorio completo** con `deleteDirectory('solicitudinvitado/'.$idSolicitud)`.

> Para la purga, **borrar el directorio completo es lo correcto y ya hay precedente en el proyecto.** Reutiliza ese enfoque, no inventes otro.

---

## Lo que hay que construir

### 1. Un comando artisan

Sigue el patrón de los seis que ya existen en `app/Console/Commands/` (mira `ReservacionesExpirarTemporales` como modelo más cercano: barrido por fecha con efectos en cascada).

**Requisitos del comando:**

| Requisito | Por qué |
|---|---|
| **`--dry-run` obligatorio** | Es un borrado irreversible de datos personales. Debe poder ejecutarse en modo informe antes de la primera corrida real. **Que sea el comportamiento por defecto si lo consideras correcto** |
| **Lote acotado** (`--limite`, con tope sensato) | Un primer arranque puede encontrar meses de acumulación. Borrar todo en una transacción gigante es un riesgo innecesario |
| **Idempotente** | Volver a correrlo no debe fallar ni duplicar efectos |
| **Transacción por solicitud, no por lote** | Una solicitud problemática no debe tumbar el barrido completo |
| **Resumen al final** | Cuántas purgadas, cuántas retenidas y **por qué motivo** cada retención |

### 2. Criterios de selección

**Se purga una solicitud cuando se cumple TODO:**

- `solicitudFechaSalida` estrictamente anterior a hoy, menos un margen de gracia configurable (ver hallazgo 1)
- La transferencia a Tlalpan está consumada: `idSolicitudTlalpan` presente (hallazgo 2)
- No queda nada pendiente en el outbox de cancelaciones — revisa `Solicitud::cancelacionesPendientes()` y `cerrarTransferencia()` para determinar la condición exacta
- Si tiene reservación asociada (`rsv_reservacion.idSolicitud`): **no** está en `ESTADOS_EN_VUELO` y **no** tiene marcas abiertas en `rsv_replicacion` (hallazgo 3)

**Caso aparte — solicitudes `Temporal` abandonadas.** Nunca se enviaron, así que nunca llegaron a Tlalpan y jamás cumplirán el criterio de transferencia: quedarían retenidas para siempre. Ya existe un concepto de reanudación de ≤ 24 h (`fetchPendiente` del front, `SolicitudController`). **Decide y justifica** si les corresponde una regla propia por antigüedad de captura. Aquí borrar sí es correcto —no hay histórico que preservar—, pero es una regla distinta y debe quedar explícita.

### 3. Qué se elimina

Determina el alcance leyendo el esquema real, **no supongas el comportamiento de las llaves foráneas**: verifica si hay `ON DELETE CASCADE` o si hay que borrar en orden explícito.

Como mínimo entra: la solicitud, sus invitados, el directorio completo de documentos, y —si aplica— la reservación con sus habitaciones, extras y paquetes.

**Decide explícitamente y justifica** qué pasa con:
- `rsv_bitacora` — es el rastro de auditoría local. Borrarlo elimina la evidencia de que la purga ocurrió
- `rsv_replicacion` ya en estado `Procesado`
- Las notificaciones y sus logs

> Sugerencia a evaluar: **la bitácora debería sobrevivir a la purga**, y la purga misma debería dejar su propio asiento. Un borrado de datos personales sin rastro de que ocurrió es un problema de auditoría, no una virtud. Pero un asiento de bitácora **no debe reintroducir los datos personales que se están borrando**.

### 4. Programación

Registra el comando en `routes/console.php` siguiendo el estilo de los que ya están: comentario explicando **qué barre, por qué a esa frecuencia y qué documento lo respalda**.

**Una vez al día, en horario de baja actividad.** No cada minuto: nada de esto es urgente y el barrido lee toda la tabla. Usa `withoutOverlapping()` como el resto.

### 5. Configuración

Los valores que el negocio puede querer mover van a `config/` con su `env()`, en el estilo comentado de `config/invitados.php`:

- Días de gracia después de la fecha de salida (por defecto, el mínimo que satisface el hallazgo 1)
- Tamaño del lote
- Interruptor de apagado de emergencia

### 6. Documentación

- `CONFIGURACION_DOCUMENTOS_PORTAL.md` §5 dice hoy *"Mientras D6 no se autorice, nada borra estos archivos"*. **Actualízalo**: describe el criterio, la frecuencia y cómo verificarla
- `NOTA_COORDINACION_DOCUMENTOS_TLALPAN.md` §5 — es la sección que el equipo de Tlalpan lee sobre conservación, y **cierra diciendo que cuando el Portal implemente la purga esta nota se actualiza con la fecha de entrada en vigor y los días acordados, ANTES de que ocurra el primer borrado**. Cúmplelo: es un compromiso escrito con el otro equipo

---

## Decisiones que NO son tuyas — repórtalas, no las inventes

La documentación es explícita en que la política de conservación **es del negocio, no del código**. Deja estas preguntas planteadas en tu reporte:

1. **¿Días de gracia?** Un socio que reclama una semana después ya no encontrará su solicitud en el Portal.
2. **¿La reservación se purga con el mismo criterio que la solicitud de invitados?** Comparten `idSolicitud` pero **son fechas distintas** (`reservacionFechaSalida` vs `solicitudFechaSalida`) y pueden no coincidir. **Verifica cuál manda, no asumas que son iguales.**
3. **¿Alguien debe recibir el resumen del barrido**, o basta con el log?

---

## Lo que NO debes hacer

- ❌ Borrar una solicitud que no llegó a Tlalpan (hallazgo 2)
- ❌ Borrar una reservación en vuelo o con outbox abierto (hallazgo 3)
- ❌ Tocar el canal de transferencia. La purga **observa** su estado; no marca, no cierra, no reintenta
- ❌ Un primer despliegue sin `--dry-run` ejecutado y revisado
- ❌ Borrar con un `DELETE` masivo por fecha sin comprobar los criterios uno por uno
- ❌ Suponer el comportamiento de las FKs sin leer el esquema
- ❌ Borrar el rastro de auditoría sin decidirlo explícitamente
- ❌ Escribir en `storage/app/private` fuera del directorio de la solicitud que estás purgando. **La ruta se arma con enteros de nuestra propia base, nunca con datos de entrada**

---

## Verificación

1. **`--dry-run` sobre datos reales de pre-producción**: revisa el listado completo antes de cualquier borrado
2. Solicitud vencida y transferida, sin reservación → **purgada**, con su directorio borrado
3. Solicitud vencida y transferida, con reservación finalizada y replicada → **purgada por completo**
4. Solicitud vencida **sin** `idSolicitudTlalpan` → **retenida y reportada**
5. Solicitud vencida con reservación en `Confirmando` → **retenida**
6. Solicitud vencida con marca `Pendiente` en `rsv_replicacion` → **retenida**
7. Solicitud con fecha de salida **hoy** → **no se toca** (hallazgo 1)
8. Correr el comando dos veces seguidas → la segunda no falla y reporta cero
9. Tras purgar, confirma que el log del Portal **no** se llena de `veredicto sobre una solicitud inexistente` en el ciclo siguiente de Tlalpan. **Es la comprobación empírica del hallazgo 1**
10. Confirma que el directorio quedó borrado en disco, no solo el registro en base

## Al terminar, reporta

1. Comando, configuración y archivos tocados
2. **El criterio exacto de selección que implementaste**, escrito en prosa
3. Qué decidiste sobre las `Temporal` abandonadas, la bitácora y el alcance del borrado, con motivos
4. Resultado de las pruebas 4, 5, 6, 7 y 9 — son las que demuestran que la purga no destruye histórico
5. Las tres preguntas de negocio, planteadas para que alguien las responda
6. Cuánto espacio liberó el `--dry-run` sobre datos reales: es el dato que justifica el trabajo
