Saltar a contenido

Restaurante, salas y mesas

Esta página explica el dominio físico y organizativo del sistema: el restaurante (con su base de conocimiento para el chat), las salas (Room) que lo dividen y las mesas (Table) con su código QR. Es la raíz de toda la multitenancy: casi todo en la plataforma cuelga, directa o indirectamente, de un restaurant_id.

Para el modelado hexagonal subyacente, ver Arquitectura hexagonal. Para cómo nace una sesión al escanear el QR, ver Sesiones y carrito.

Visión general

El modelo físico tiene tres niveles anidados:

Restaurant  (1) ──< Room  (N) ──< Table (N)
  • Un Restaurant agrupa una o varias salas (Room).
  • Una sala agrupa varias mesas (Table).
  • Cada mesa tiene un qr_code persistente que el cliente escanea para abrir una sesión.

Estos datos viven en dos módulos distintos que no se importan entre sí (regla ADR-004): el módulo restaurant (que posee Restaurant, RestaurantKnowledge y Room) y el módulo table (que posee Table). La comunicación cross-módulo se hace solo vía puertos compartidos declarados en app/shared/application/ports.py.

Módulo restaurant

La entidad Restaurant

Definida en app/modules/restaurant/domain/entities.py (ORM en app/modules/restaurant/infrastructure/models/restaurant_model.py). Representa una localización real de restaurante y lleva su propia configuración multi-tenant: identidad (name, slug, address, phone, city, postal_code, country, email, logo_url), ajustes operativos (tax_rate, currency, timezone, default_language, supported_languages) y propiedad (owner_id, created_by).

El ciclo de vida se modela con el value object RestaurantStatus (app/modules/restaurant/domain/value_objects.py):

Estado Valor Significado
ACTIVE active Operativo y visible para clientes.
SUSPENDED suspended Deshabilitado temporalmente (impago, revisión).
PENDING pending A la espera de activación tras crearse.
ARCHIVED archived Cerrado permanentemente; datos retenidos para auditoría.

El dominio expone operaciones con invariantes en vez de mutar campos sueltos: rename() (renombra y regenera el slug vía el VO Slug), change_status(), soft_delete() (borrado lógico vía SoftDeleteMixin, no físico) y touch().

Banderas de comportamiento

Tres campos del restaurante gobiernan cómo se comportan el pedido y el chat:

  • order_approval_mode (OrderApprovalMode, app/shared/domain/enums.py): cómo se enruta una comanda enviada antes de llegar a cocina.

    Valor Comportamiento
    FULL (full) Validación IA + aprobación del manager (por defecto).
    AI_ONLY (ai_only) Solo validación IA; sin aprobación del manager.
    NONE (none) Va directo a cocina; se omite la validación IA.

    El módulo comanda lee este modo a través del puerto IRestaurantPolicyGateway (no importa el restaurante). Ver Comanda.

  • chat_mode (ChatEngineMode): motor de chat que corren las mesas del restaurante.

    Valor Comportamiento
    LEGACY (legacy) Flujo 100% LLM (por defecto).
    HYBRID (hybrid) Intérprete de árbol de decisión + clasificador de intents.

    Es el flag que conmuta la arquitectura híbrida del chat; el rollback es instantáneo volviendo a legacy.

  • ai_proactive_enabled: vive en la base de conocimiento (ver más abajo) y habilita la apertura proactiva del bot y los nudges de coursing.

Defaults

order_approval_mode y chat_mode tienen server_default (full / legacy) en la columna, de modo que los restaurantes existentes mantienen el comportamiento legacy.

RestaurantKnowledge — base de conocimiento para el chat

Definida en la misma entities.py (ORM RestaurantKnowledge, tabla restaurant_knowledge, relación 1:1 con el restaurante vía restaurant_id único).

Su propósito es constreñir al LLM a datos verificados para evitar alucinaciones: el asistente solo puede usar esta información al responder preguntas del cliente. Agrupa:

  • Identidad y ubicación: description, cuisine_type, tagline, full_address, city, postal_code, country, coordinates, website.
  • Horarios y contacto: opening_hours, contact_phone, contact_email.
  • Políticas: reservation_policy, cancellation_policy, dress_code, payment_methods, accessibility_info, parking_info, pet_policy, children_policy.
  • Servicios (booleanos): has_terrace, has_private_rooms, has_wifi, has_parking, has_delivery, has_takeout, accepts_groups (con max_group_size), accepts_reservations.
  • FAQs y eventos: faqs, current_events, additional_info.
  • Personalidad de la IA: ai_greeting (y variantes por franja: ai_greeting_morning/afternoon/evening/night), ai_personality_notes, ai_tone (def. warm), ai_formality (def. semi-formal), ai_enthusiasm (def. 0.75), ai_suggestion_phrases, ai_confirmation_phrases.
  • Proactividad: ai_proactive_enabled (def. True) y ai_proactive_max_per_session (def. 5).
  • Respuesta de respaldo: unknown_info_response, el texto que da el bot cuando no conoce un dato (en vez de inventarlo).

Cómo lo consume el chat

El chat no lee este ORM directamente. Lee un snapshot de solo-lectura (RestaurantInfoSnapshot) a través del puerto IRestaurantInfoGateway — ver Puertos compartidos.

Room — salas

Definida en entities.py (ORM en app/modules/restaurant/infrastructure/models/room_model.py, tabla rooms). Una sala es una sección física del restaurante que agrupa mesas. Lleva restaurant_id, name y un position explícito para controlar el orden de visualización en el frontend. Borrar un restaurante elimina sus salas en cascada (ondelete="CASCADE").

Módulo table

La entidad Table

Definida en app/modules/table/domain/entities.py (ORM en app/modules/table/infrastructure/models/table_model.py, tabla tables). Una mesa pertenece a una Room (vía room_id, propiedad del módulo restaurant) y lleva:

  • name: nombre legible de la mesa (p. ej. "12", "Terraza 3").
  • qr_code: identificador QR persistente (VO QrCode, app/shared/domain/value_objects.py; no puede ser vacío). En el ORM es un uuid4 único generado por defecto.
  • default_customers: número de comensales por defecto para nuevas sesiones.
  • position: orden de visualización dentro de la sala.
  • is_active: si la mesa es visible para lookup público y puede generar sesiones.

El dominio expone rename(), set_capacity(), activate() y deactivate(). Las mesas desactivadas quedan ocultas a las búsquedas públicas y no pueden crear nuevas sesiones. La mesa usa SoftDeleteMixin (borrado lógico).

El código QR y QRService

El qr_code de la columna es solo un identificador persistente. El QR físico que escanea el cliente lleva un token firmado y con caducidad, generado y validado por QRService (app/modules/session/infrastructure/services/qr_service.py).

El token NO es un JWT estándar: es un payload JSON base64url-codificado más una firma HMAC-SHA256 con la secret_key de la configuración, en el formato payload_b64.signature. El payload contiene restaurant_id, table_id, room_id, un nonce y expires_at. La caducidad por defecto es QR_EXPIRY_HOURS = 24 horas.

  • generate_token(restaurant_id, table_id, room_id) construye y firma el token.
  • validate_token(token) verifica la firma con hmac.compare_digest, comprueba la caducidad y devuelve el payload o None (rechazo). Mantiene compatibilidad hacia atrás con tokens antiguos restaurando el padding base64.

Dónde vive QRService

Aunque firma datos de mesa, QRService reside en el módulo session porque es ahí donde nace la sesión al validar el QR. Ver Sesiones y carrito.

Multitenancy por restaurant_id

Toda la plataforma está particionada por restaurante:

  • Las queries de los servicios filtran siempre por restaurant_id.
  • Nunca se confía en el restaurant_id del body de una petición: se obtiene de la autenticación o de un path param verificado.
  • El acceso admin a un restaurante se comprueba con el puerto IRestaurantAccessChecker (un SUPER_ADMIN lo bypassa).

Ver Seguridad y multitenancy para el detalle de los guards.

Puertos compartidos cross-módulo

Como restaurant y table no pueden importarse entre sí ni desde otros módulos (ADR-004), exponen sus datos mediante puertos definidos en app/shared/application/ports.py. Cada puerto se implementa con un adapter dentro del módulo dueño y se registra al arrancar mediante un factory (register_* / get_*).

Puerto Lo implementa Lo consume Para qué
IRoomDirectory restaurant table Resolver/crear salas (find_room, get_or_create_default_room, list_room_ids) y obtener datos de display del restaurante (get_restaurant_display), devolviendo DTOs (RoomInfo, RestaurantDisplayInfo).
ITableDirectory table session Resolver el nombre de una mesa (get_table_name) para mostrarlo (p. ej. la cabecera "Mesa N" en la app del cliente) sin importar el ORM de table.
IRestaurantInfoGateway restaurant chat Leer las facts de la base de conocimiento como RestaurantInfoSnapshot (get_info), para que las skills conversacionales (p. ej. consulta_local) nunca toquen el ORM.
IRestaurantPolicyGateway restaurant comanda Leer la política de envío como RestaurantPolicyInfo (get_policy): order_approval_mode, ai_proactive_enabled y chat_mode.
IRestaurantAccessChecker restaurant Routers admin Verificar acceso multi-tenant (check_access → RestaurantAccessInfo, list_accessible_ids) en vez de importar require_restaurant_access.

Patrón de registro

Los puertos sin estado de sesión (IRoomDirectory, ITableDirectory, IRestaurantAccessChecker) usan factories sin argumentos. Los que necesitan consultar la BD (IRestaurantInfoGateway, IRestaurantPolicyGateway) reciben la AsyncSession de la petición. Llamar al get_* sin haber registrado el factory lanza RuntimeError.

Véase también