CHEFMANAGER IA · V3.10.0
FOOD COURT FASE 5 (LIQUIDACIONES) + INTERFAZ DE USUARIO BÁSICA DE TODO EL MÓDULO
=================================================================================

RESUMEN

Esta entrega cierra el backend del módulo de Patio de Comidas con la Fase 5
(liquidaciones por período) y agrega, por primera vez, una interfaz de
usuario para las cinco fases: una página pública de directorio y pedido,
una página pública de seguimiento, y un panel de administración básico.
También corrige dos errores reales encontrados durante esta ronda de
trabajo (ver "CORRECCIONES DE ESTA ENTREGA" más abajo).

1) FASE 5 · LIQUIDACIONES

Una liquidación (food_court_settlements) es una fotografía de un período
(period_start..period_end) para un patio de comidas completo. Por cada
operador con al menos un subpedido pagado en ese período (o con alquiler
fijo pactado aunque no haya tenido ventas), calcula:

- orders_count: cantidad de subpedidos pagados en el período.
- gross_sales: venta bruta de esos subpedidos (orders.total).
- centrally_collected: cuánto de esa venta cobró el patio de forma
  central (Fase 4, food_court_order_payment_operators).
- locally_collected: gross_sales - centrally_collected (lo que el propio
  negocio cobró en su caja).
- commission_rate / commission_total: comisión pactada (%) sobre
  gross_sales.
- rent_total: alquiler fijo pactado para el período.
- net_payable: centrally_collected - commission_total - rent_total.
  Positivo = el patio le debe ese monto al operador (de lo que cobró en
  su nombre y no le ha entregado). Negativo = el operador le debe ese
  monto al patio (comisión/alquiler no cubiertos por lo cobrado de forma
  central).
- amount_patio_owes_operator / amount_operator_owes_patio: la misma cifra
  separada en dos columnas siempre positivas, para no depender del signo
  al mostrarla.

Una liquidación NO mueve dinero, no genera pagos y no toca `sales` ni
`payments`: es un reporte para que el patio y cada negocio concilien
manualmente lo que corresponde. Se genera en estado "draft" (puede
regenerarse mientras esté en ese estado) y se cierra explícitamente a
"final" con PUT, momento en el que queda bloqueada para siempre (un
intento posterior de modificarla se rechaza con 422).

El contrato de liquidación de cada operador (comisión % y alquiler fijo)
vive en food_court_operators.commission_percent/rent_amount, dos columnas
nuevas. Solo quien administra el patio (Control Center global o un
manager acotado a ese patio) puede fijarlas, con el mismo PUT
/food-courts/{id}/operators/{id} de las fases anteriores; la rama de
autogestión del propio negocio (cuando acepta o abandona su participación)
no las toca en absoluto, por diseño: un negocio no puede autoasignarse su
propia comisión o alquiler.

Endpoints nuevos (recurso food-courts, ya existente):

- GET  /food-courts/{id}/settlements            Lista de liquidaciones.
- POST /food-courts/{id}/settlements            Genera una liquidación
  para un período (period_start, period_end). Rechaza períodos inválidos
  (falta alguno de los dos, o el fin no es posterior al inicio).
- GET  /food-courts/{id}/settlements/{sid}       Detalle: la liquidación y
  la fila calculada de cada operador.
- PUT  /food-courts/{id}/settlements/{sid}       Con {"status":"final"}
  cierra la liquidación. Cualquier otra modificación sobre una liquidación
  ya cerrada se rechaza.

2) INTERFAZ DE USUARIO BÁSICA (nueva en esta entrega, todas las fases)

Hasta la V3.9.0 el módulo completo (Fases 1 a 4) solo podía operarse por
API. Esta entrega agrega tres páginas independientes, con el mismo patrón
que menu.php/kiosk.php/tracking.php (un shell PHP + JS plano, sin tocar el
bundle principal de la SPA en public/assets/app.js):

- food-court.php · Directorio público y pedido. Muestra los operadores de
  un patio (por slug o código) y su catálogo agregado, con un carrito por
  operador y checkout, usando exactamente la acción "food-court-order" de
  la Fase 2 (sin cambios de backend). Marca visualmente el local cerrado y
  bloquea agregar productos de un local cerrado.
- food-court-tracking.php · Seguimiento público del pedido maestro, usando
  la acción "food-court-track" de la Fase 3: estado conjunto, estado de
  pago y detalle por operador con sus productos.
- food-court-admin.php · Panel de administración básico, con sesión y
  CSRF del propio Control Center (auth/login, auth/me, auth/logout):
    - Vista de administrador (Control Center global o manager acotado a
      un patio): elegir/crear patio, pestaña "Operadores" (listar,
      invitar por restaurant_id/branch_id, editar estado/visibilidad/
      comisión/alquiler), pestaña "Pedidos" (listar pedidos maestros,
      ver detalle con subpedidos e ítems, registrar un pago central sobre
      el saldo pendiente), pestaña "Liquidaciones" (listar, generar por
      período, ver detalle con la tabla por operador, cerrar).
    - Vista de negocio (tenant): lista de patios en los que participa su
      restaurante, con un botón para unirse (si está "pending") o salir
      (si está "active") de cada uno, usando la misma autogestión de la
      Fase 1.

Estas páginas son deliberadamente básicas: sin variantes/opciones de
producto en el carrito del patio (el catálogo agregado de Fase 1 ya no
incluye esa complejidad), sin edición de datos del patio (logo, banner,
dirección) desde el panel, y sin paginación en las tablas de pedidos o
liquidaciones. Cubren el flujo completo de cliente y de administración
mínima necesaria para operar un patio de comidas real.

3) CORRECCIONES DE ESTA ENTREGA (errores reales, no solo de diseño)

a) PUT /food-courts/{id}/operators/{id} borraba el estado del operador
   al actualizar solo comisión/alquiler. El código calculaba el nuevo
   estado con
     $status = in_array($body['status']??$op['status'], [...], true)
       ? (string)$body['status'] : (string)$op['status'];
   Si la petición no incluía "status" (como la del panel al guardar solo
   comisión y alquiler), la rama verdadera del ternario igual leía
   $body['status'], que no existía: PHP 8 lo trata como null y lo
   convierte a cadena vacía, dejando food_court_operators.status='' en
   la base de datos. El efecto visible: el operador desaparecía del
   directorio público y cualquier intento de pedirle algo fallaba con
   "El operador ya no está disponible en este patio de comidas", aunque
   momentos antes se hubiera activado correctamente. Se encontró de forma
   real construyendo el escenario de prueba de la Fase 5 (activar un
   operador, fijarle comisión y alquiler, e intentar pedirle), no por
   lectura de código. Corregido guardando primero el valor candidato en
   una variable y usándola en ambas ramas del ternario.
b) El mismo patrón (y el mismo bug) existía en PUT /food-courts/{id}
   (podía borrar el estado del propio patio de comidas al actualizar solo
   su descripción, por ejemplo) y, fuera del módulo de patios de comidas,
   en PUT /franchises/{id} y PUT /franchises/{id}/members/{id} (podían
   borrar el estado de una franquicia o de una membresía al actualizar
   otro campo sin enviar "status"). Se encontraron revisando el código
   fuente en busca de este mismo patrón después de dar con el primero, no
   por prueba HTTP directa sobre franquicias; se corrigieron con el mismo
   cambio mínimo. Ver docs/VALIDACION-3.10.0.txt para la verificación
   puntual sobre patios de comidas.

ALCANCE NO INCLUIDO TODAVÍA

- Reversa o anulación de una liquidación ya cerrada.
- Edición del logo/banner/dirección de un patio desde el panel (sí es
  posible por API, PUT /food-courts/{id}).
- Paginación en las tablas de pedidos y liquidaciones del panel (hoy
  limitadas por el backend a 200 filas más recientes).
- Carrito con variantes/opciones de producto en la página pública del
  patio (el catálogo agregado nunca las expuso, desde la Fase 1).
- Reversa o anulación de un pago central ya aplicado (heredado de la
  Fase 4).
- Pago parcial de un subpedido individual (heredado de la Fase 4).

ANTES DE PRODUCCIÓN

- Definir con cada patio real el contrato de comisión/alquiler de cada
  operador antes de generar la primera liquidación.
- Revisar visualmente el panel de administración con un patio con varios
  operadores y volumen real de pedidos.
- Homologar ClickDelivery con la documentación y credenciales oficiales.
- Probar respaldo/restauración y los cron worker/cleanup.
- Configurar y comprobar AI Gateway/Ollama con secreto compartido.
