CHEFMANAGER IA · V3.12.0
RESERVAS POR ENLACE DE WHATSAPP
CONTROL REAL DE HORARIO Y CUPO PARA RESERVAS PÚBLICAS
================================================================================

RESUMEN

Esta entrega agrega la forma más pedida de tomar reservas por WhatsApp: el
negocio genera un enlace corto desde el panel, lo pega en un chat de
WhatsApp (manual o, a futuro, mediante un bot autorizado que llame el
mismo endpoint), y el cliente lo abre en una página pública mobile-first
donde elige fecha, horario y número de personas, sin necesitar cuenta.
Al confirmar, ChefManager verifica disponibilidad real (horario de la
sucursal + cupo de mesas) antes de aceptar la reserva y encola un mensaje
de confirmación por WhatsApp Business reutilizando la misma cola que ya
usan pedidos y delivery.

ChefManager IA ya tenía un módulo de Reservas (tabla `reservations`,
CRUD, agenda). Esta entrega NO lo duplica: agrega únicamente lo que
faltaba para el caso de uso "reserva por WhatsApp sin cuenta" — el enlace
seguro de un solo propósito, la página pública con calendario/horarios
reales, y el control de cupo que antes no existía en ningún punto del
sistema.

1) ENLACES DE RESERVA (`reservation_links`)

Tabla nueva (aditiva, no rompe nada): `reservation_links(id,
restaurant_id, branch_id NULL, token ÚNICO, customer_name NULL,
customer_phone NULL, notes NULL, max_uses NULL, used_count, expires_at
NULL, revoked_at NULL, created_by, created_at)`.

El `token` es un valor aleatorio opaco (`bin2hex(random_bytes(16))`),
exactamente el mismo idioma que ya usa el sistema para enlaces públicos
de un solo propósito (`orders.public_token`). Se decidió deliberadamente
NO usar JWT: ChefManager no tiene ni necesita una dependencia de JWT para
esto, y un token opaco de un solo uso resuelto por `WHERE token=?` cumple
el mismo requisito de seguridad (impredecible, revocable, sin datos
codificados que se puedan inspeccionar) con el patrón que el resto del
código ya usa y ya audita.

Un enlace puede:
- Ser genérico (para compartir con cualquier cliente) o venir precargado
  con nombre/teléfono de un cliente específico (autocompleta el
  formulario, pero el cliente puede editarlo).
- Fijarse a una sucursal concreta, o dejarse en la sucursal principal.
- Tener un número máximo de usos (`max_uses`) y/o fecha de vencimiento
  (`expires_at`).
- Revocarse en cualquier momento desde el panel (`revoked_at`); un enlace
  revocado, vencido o agotado responde con un error claro y específico
  (410/404) en vez de aceptar la reserva o de un error genérico.

Administración: `POST/GET /reservation-links` (crear/listar, requiere
permiso `reservations.manage`) y `DELETE /reservation-links/{id}`
(revocar). Nueva página "Reservas por WhatsApp" en el panel (bajo
Operación diaria): genera el enlace, copia la URL al portapapeles,
muestra usos/vencimiento/estado y permite revocar.

2) PÁGINA PÚBLICA DE RESERVA (`reserva.php`)

Página mobile-first, sin necesidad de cuenta, siguiendo el mismo patrón
de páginas públicas ya usado en `tracking.php` (shell PHP delgado +
`reserva.js` que hace todo el trabajo contra `api/public.php`):

- Calendario (input de fecha nativo, limitado a hoy .. +90 días).
- Selector de personas (contador +/-, limitado a la capacidad real de la
  sucursal).
- Botones de horario: se calculan en tiempo real según el horario de
  atención de la sucursal para esa fecha exacta (no "ahora", como ya
  existía) y el cupo restante en cada franja; los horarios sin cupo
  suficiente se muestran deshabilitados en vez de ocultarse, para que el
  cliente entienda por qué no puede elegirlos.
- Nombre/teléfono precargados si el enlace los trae, editables siempre.
- Comentarios opcionales.
- Pantalla de confirmación o de error (enlace inválido/vencido/revocado/
  agotado, o el horario elegido ya no tiene cupo — puede pasar si dos
  personas reservan el mismo horario casi al mismo tiempo).

3) CONTROL REAL DE HORARIO Y CUPO (corrección de una brecha existente)

Antes de esta entrega, NINGÚN punto del sistema —ni el flujo público
anterior, ni el CRUD administrativo de reservas— validaba que una
reserva nueva cupiera en el horario de atención o en la capacidad de
mesas de la sucursal. Se podían crear, sin ningún aviso, cinco reservas
de 20 personas cada una para el mismo horario en un salón con capacidad
para 30.

Se agregó `ReservationLinkService` con:

- `scheduleWindows()`: generaliza la lógica ya existente de horario
  "abierto ahora mismo" (`pubOpenState`) a una fecha arbitraria futura,
  respetando la misma prioridad: excepción puntual (`business_hour_
  overrides`) > horario de la sucursal > horario general del restaurante
  > 08:00–22:00 por defecto.
- `branchCapacity()`: suma la capacidad de las mesas activas de la
  sucursal (`restaurant_tables`); si el negocio aún no cargó su plano de
  mesas, usa un tope configurable (`reservations.max_concurrent_guests`,
  40 por defecto) para no bloquear a un negocio que todavía no configuró
  su salón.
- `availableSlots()`: genera los horarios reservables del día (cada
  `reservations.slot_interval_minutes` minutos, turnos de
  `reservations.slot_duration_minutes` minutos — 30 y 90 por defecto),
  con el cupo restante de cada uno según las reservas ya existentes que
  se superponen con ese turno.
- `assertSlotAvailable()`: se ejecuta al confirmar una reserva; si el
  horario elegido ya cerró, no da tiempo de comer antes del cierre, o ya
  no tiene cupo para la cantidad de personas solicitada, rechaza la
  reserva con 409 y un mensaje accionable en vez de aceptarla a ciegas.

ALCANCE DE ESTA CORRECCIÓN: se aplicó únicamente al flujo público/de
enlace de WhatsApp (`POST /api/public.php?action=reservation`), que hoy
no tenía ningún llamador real en producción, por lo que fortalecerlo es
seguro. El CRUD administrativo genérico de reservas
(`ExtendedModules.php`, usado por el panel para creación manual por el
propio negocio) NO se tocó — un administrador puede seguir forzando una
reserva fuera de horario o por encima del cupo si así lo decide
conscientemente (por ejemplo, una reserva especial con mesas adicionales
prestadas). Extender la misma validación al CRUD administrativo, con la
posibilidad de que el negocio la anule explícitamente, queda como mejora
futura fuera del alcance de esta entrega.

4) CONFIRMACIÓN AUTOMÁTICA POR WHATSAPP

`ExternalChannelsService::onReservationCreated()` se dispara justo
después de crear la reserva (venga del enlace de WhatsApp o del
formulario público) y encola un mensaje de confirmación reutilizando
exactamente la misma infraestructura que ya usan pedidos y delivery:
tabla `integration_jobs`, plantilla configurable en
`restaurant_settings` (`whatsapp_reservation_confirmed`, con variables
`{cliente}`, `{personas}`, `{fecha}`, `{hora}`, `{sucursal}`), campo de
plantilla oficial del proveedor opcional
(`whatsapp_reservation_confirmed_template`), y el mismo idioma
(`whatsapp_template_language`) que las demás plantillas. El worker
existente (`cron/worker.php` → `IntegrationService`) procesa este job
igual que los demás — no se agregó ninguna lógica de envío nueva.

IMPORTANTE: esto solo funciona si el negocio ya conectó WhatsApp Business
API (Meta Cloud API) en Integraciones. Si no está configurado o activo,
la reserva se crea normalmente y simplemente no se encola ningún mensaje
— no se produce ningún error ni se bloquea la reserva.

La nueva plantilla es editable desde la página WhatsApp del panel
(sección "Automatización del pedido" y, para el nombre de plantilla
oficial, dentro de "Plantillas oficiales del proveedor").

5) BASE DE DATOS (cambios aditivos)

- Tabla nueva `reservation_links` (ver punto 1).
- `reservations.reservation_link_id` (INTEGER NULL): qué enlace originó
  la reserva, si vino de uno.
- `reservations.confirmed_via` (TEXT NULL): `'whatsapp_link'` o `'web'`
  para reservas creadas por el flujo público; NULL para las creadas por
  el panel administrativo, como siempre.

Ningún cambio de esquema existente se modificó ni se volvió obligatorio;
las reservas ya existentes no se ven afectadas.

ALCANCE NO CUBIERTO EN ESTA ENTREGA (decisión consciente)

- Un bot de WhatsApp que genere el enlace automáticamente al recibir un
  mensaje del cliente (por ejemplo, vía Meta Cloud API + palabra clave):
  el endpoint de creación de enlaces ya existe y es reutilizable para
  eso, pero conectar un webhook entrante de WhatsApp a este flujo queda
  fuera de esta entrega.
- Edición de una reserva ya creada desde la página pública (cambiar
  fecha/hora/personas sin generar una reserva nueva): el cliente puede
  volver a usar el enlace si le queda algún uso disponible, pero no hay
  un flujo dedicado de "modificar mi reserva".
- Extender la validación de horario/cupo al CRUD administrativo de
  reservas (ver punto 3, alcance de la corrección).
- Recordatorios automáticos por WhatsApp antes de la hora de la reserva
  (solo se envía la confirmación inmediata al crear la reserva).
