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:
- Un
Restaurantagrupa una o varias salas (Room). - Una sala agrupa varias mesas (
Table). - Cada mesa tiene un
qr_codepersistente 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
comandalee este modo a través del puertoIRestaurantPolicyGateway(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(conmax_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) yai_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 (VOQrCode,app/shared/domain/value_objects.py; no puede ser vacío). En el ORM es unuuid4ú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 conhmac.compare_digest, comprueba la caducidad y devuelve el payload oNone(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_iddel 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(unSUPER_ADMINlo 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.