Diagramas de datos e infraestructura¶
Verificados contra el código el 2026-10-10 (modelos SQLAlchemy, compose, nginx, auth y manager). Solo hechos del código; las dudas van anotadas. Convención de dinero: céntimos enteros (
price,total,*_cents).
1. Modelo de datos (ER simplificado)¶
Tablas leídas de backend/app/modules/*/infrastructure/models/. Se dibujan
las FK reales; bills.session_token no tiene FK (solo índice) y
payment_idempotency_keys es standalone con unique (scope, key).
erDiagram
RESTAURANTS ||--o{ ROOMS : tiene
ROOMS ||--o{ TABLES : contiene
TABLES ||--o{ TABLE_SESSIONS : abre
RESTAURANTS ||--o{ TABLE_SESSIONS : opera
RESTAURANTS ||--o{ MENUS : publica
MENUS ||--o{ CATEGORIES : agrupa
CATEGORIES ||--o{ PRODUCTS : lista
TABLE_SESSIONS ||--o{ CART_ITEMS : acumula
PRODUCTS ||--o{ CART_ITEMS : referencia
TABLE_SESSIONS ||--o{ ORDERS : genera
ORDERS ||--o{ ORDER_LINES : detalla
PRODUCTS ||--o{ ORDER_LINES : referencia
TABLE_SESSIONS ||--o{ COMANDA_COURSE_PLAN : planifica
RESTAURANTS ||--o{ BILLS : factura
TABLES ||--o{ BILLS : consume
BILLS ||--o{ BILL_PAYMENTS : cobra
RESTAURANTS ||--o{ CONVERSATIONS : conversa
CONVERSATIONS ||--o{ CHAT_MESSAGES : guarda
CONVERSATIONS ||--o{ CHAT_LOGS : audita
RESTAURANTS ||--o{ CONVERSATION_TREES : versiona
CUSTOMERS ||--o{ ORDERS : pide
CUSTOMERS ||--o{ CONVERSATIONS : chatea
TABLE_SESSIONS ||--o{ REVIEWS : valora
TABLE_SESSIONS ||--o{ MANAGER_CALLS : avisa
ADMIN_USERS ||--o{ MANAGER_CALLS : atiende
ADMIN_USERS }|--|{ RESTAURANTS : asignado
RESTAURANTS ||--|| RESTAURANT_KNOWLEDGE : describe
RESTAURANTS {
int id PK
string slug_uk "unique"
float tax_rate
string currency
}
TABLES {
int id PK
string qr_code_uk "unique"
int default_customers
}
TABLE_SESSIONS {
int id PK
string session_token_uk "unique"
string lifecycle_state
int version
}
PRODUCTS {
int id PK
json name "i18n"
int price "céntimos"
string course_type
}
ORDERS {
int id PK
string status "draft..."
int total "céntimos"
int version
}
BILLS {
int id PK
int base_total_cents
int tip_cents
string status
}
CONVERSATION_TREES {
int id PK
int version
string status "active único"
json nodes
}
Notas:
TableLifecycleState(manager, sin tabla propia) persiste entable_sessions.lifecycle_state:READING → DRINKS_ORDERED → DRINKS_DELIVERED → FOOD_ORDERED → FOOD_DELIVERED → BILL_REQUESTED → CLOSED, másDELAY_ALERTtransversal (manager/domain/lifecycle.py).Comandavive en la tablaorders;ComandaLineenorder_lines;comanda_model.pyes solo shim de re-export.
2. Despliegue con Docker (referencia para redesplegar)¶
Servicios leídos de backend/docker-compose.yml (prod),
backend/docker-compose.dev.yml, backend/Dockerfile (+.dev),
frontend/Dockerfile y nginx/*.conf. VPS histórico dado de baja:
el diagrama sirve para provisionar de nuevo.
flowchart TB
subgraph HOST["Host Docker (a provisionar por la Sociedad)"]
subgraph NET["Red bridge de compose"]
PG[("postgres:15-alpine\n5432 · vol pgdata\nhealthcheck pg_isready")]
BE["backend · python:3.12-slim\nuvicorn 0.0.0.0:8000\ntorch CPU · entrypoint migra\nEMBEDDINGS → /hf-cache"]
FE["frontend · node:20-alpine\n.next/standalone · :3000\nUSER nextjs · healthcheck wget"]
end
HFCACHE[("vol camarero_ia_hf_cache\n/hf-cache · ~390 MB MiniLM")]
PGDATA[("vol camarero_ia_postgres_data")]
NGINX["nginx (referencia)\nupstreams 127.0.0.1:3000/8000\nVIP 79.117.123.235 · flag mantenimiento"]
end
BE --> PG
BE --- HFCACHE
PG --- PGDATA
NGINX --> FE
NGINX --> BE
BE -.->|lifespan| JOBS["jobs: lifecycle 60 s\nworker recompute-recs"]
Notas:
- Dev (
docker-compose.dev.yml): sinhf_cacheniEMBEDDINGS_*, volumen.:/appcon reload,userns_mode: keep-id. - TLS histórico: Let's Encrypt wildcard en
n0idea.app.conf(TLS 1.2/1.3, HSTS); caducado con la baja del VPS.
3. Auth y multitenancy¶
Leído de auth/interface/{admin_router,customer_router}.py,
app/core/deps.py, jose_token_service.py,
restaurant_access_checker.py y app/core/bootstrap.py.
flowchart TB
C([Comensal]) --> CR["POST /api/v1/auth/register|login\nGET /oauth/google (PKCE)\nGET /oauth/apple (state-only)\nPOST /oauth/callback"]
A([Admin]) --> AR["GET /api/v1/admin/auth/google|apple\nPOST /api/v1/admin/auth/callback\nGET /api/v1/admin/me"]
CR --> JWT["JWT HS256 · Bearer\ncustomer 7 días (sub=email)"]
AR --> JWT24["JWT HS256 · Bearer\nadmin 24 h (sub/admin_id/role)"]
JWT --> API["endpoints cliente\n(sesión QR)"]
JWT24 --> GATE["require_restaurant_access\n404 / 403"]
GATE --> OPS["operación del restaurante"]
GATE -.->|SUPER_ADMIN| BYPASS["bypass sin asignación"]
BOOT["bootstrap ADMIN_SUPER_EMAIL\noauth_id=bootstrap (main.py:177)"] -.-> ADMINU[("admin_users")]
Notas:
- Apple
id_tokense verifica RS256 contra JWKS (apple_id_token.py, timeout 5 s); el fallo cierra (500), no degrada. - SSE de manager no usa
Authorization(EventSource no envía headers): autentica por?token=<jwt>.
4. Secuencia de control de sala¶
Leído de manager/interface/admin_manager_router.py (prefijo
/api/v1/admin/manager/...), manager/interface/background.py
(poll 60 s, delay comida 20 min, mesa silenciosa 15 min) y
frontend/lib/hooks/useManagerSessionSync.ts.
sequenceDiagram
participant M as Manager (SalaClient)
participant B as Backend /manager
participant BG as lifecycle background 60 s
participant S as SSE manager:{id}
M->>B: GET /restaurants/{id}/sessions
B-->>M: snapshot mesas + lifecycle_state
M->>S: GET /restaurants/{id}/events?token=jwt
S-->>M: sala_snapshot + table_state_changed / silent_table_alert
M->>B: POST /sessions/{token}/deliver
M->>B: POST /sessions/{token}/courtesy-round
M->>B: POST|GET /restaurants/{id}/kitchen/overload
BG->>B: FOOD_ORDERED + food_ordered_at > 20 min
B->>S: DELAY_ALERT
BG->>B: DRINKS_DELIVERED + quiet > 15 min
B->>S: silent_table_alert
5. Migraciones y seed¶
Cadena Alembic en backend/alembic/versions/ (34 ficheros, dos merges).
Se aplican con alembic upgrade head y se siembran con
scripts/seed_amici.py (reseed idempotente del restaurante Amici Madrid).
flowchart LR
A["20260416 orderlines_json … 20260521 product_decision_engine"] --> B["20260603 índices + safety01 …"]
B --> M1["20260604_merge_heads"]
M1 --> C["20260604 payment · course_plan · rate_limit · model01 · rd04 · rd07 · soft_delete"]
C --> M2["20260605_merge_rd_heads"]
M2 --> D["20260605 rdfix02 … 20260614 table_lifecycle"]
D --> SEED["seed_amici.py\nmenú · salas · mesas · KB"]