Tutorial — Flujo End-to-End de Travel
Cuadrante 01_TUTORIALS — aprende cómo funciona Travel completo siguiendo una conversación real. Pre-requisito: el ecosistema está levantado en local (ver /docs/02_HOW_TO/full-deployment-vps.md). Pre-requisito: el tenant
kasiri-01está provisionado.
Escenario
Un huésped potencial escribe a WhatsApp de Kasiri pidiendo una reserva. Vamos a seguir el flujo paso a paso, desde el mensaje hasta el código de reserva confirmado.
Paso 1: El huésped envía un mensaje
WhatsApp recibe:
"Hola, quería ver disponibilidad para 2 personas del 15 al 18 de mayo, gracias."
Evolution API empaca este mensaje y dispara un webhook a n8n.
Paso 2: n8n dispara el AI Orchestrator
n8n hace POST /api/v1/chat al AI Orchestrator (puerto 8002):
{
"session_id": "wa-573001234567",
"property_id": "kasiri-01",
"guest_phone": "+573001234567",
"message": "Hola, quería ver disponibilidad para 2 personas del 15 al 18 de mayo, gracias."
}
El header lleva Authorization: Bearer $ORCHESTRATOR_API_KEY.
Paso 3: El AI Orchestrator pide contexto al Travel Backend
El nodo load_context del grafo LangGraph llama:
GET http://travel_backend:8001/api/v1/properties/kasiri-01/context
Header: X-Internal-Token: $INTERNAL_API_KEY
Travel responde con un XML-like que describe el hotel + room_types + features (sin precios — los precios vienen aparte de /availability):
<hotel_context>
<hotel name="Kasiri Glamping" location="Eje Cafetero" altitude_m="1450">...</hotel>
<habitaciones>
<room_type id="kasiri-cafetal" name="Domo Cafetal" capacity="2">
<features>jacuzzi, vista, wifi, fireplace</features>
</room_type>
...
</habitaciones>
</hotel_context>
Paso 4: El LLM analiza el mensaje
El nodo analyze (con Groq Llama 3.1 8B-instant por velocidad) extrae:
- intent: BOOKING
- check_in: 2026-05-15
- check_out: 2026-05-18
- guests_count: 2
Paso 5: Consulta de disponibilidad
El nodo booking invoca la tool consultar_disponibilidad que hace:
GET http://travel_backend:8001/api/v1/availability
?property_id=kasiri-01
&check_in=2026-05-15
&check_out=2026-05-18
&guests=2
El availability_service en el backend:
1. Filtra rooms con reservas activas en ese rango (overlap semiabierto).
2. Excluye BlockedDate que choquen.
3. Para cada room_type disponible, calcula _effective_price() con la Season activa del rango.
4. Si fechas son temporada media (×1.15), un Domo Cafetal de 180.000 base → 207.000/noche.
Respuesta:
{
"items": [
{"room_type_id": "kasiri-cafetal", "name": "Domo Cafetal", "base_price": 180000, "effective_price": 207000, "available_count": 1},
{"room_type_id": "kasiri-guadua", "name": "Domo Guadua", "base_price": 220000, "effective_price": 253000, "available_count": 2}
]
}
Paso 6: Bot le muestra opciones al huésped
El Orchestrator responde a n8n con un mensaje formateado:
"Tenemos disponibles para esas fechas: 🌿 Domo Cafetal — 207.000/noche × 3 = $621.000 🎋 Domo Guadua — 253.000/noche × 3 = $759.000 ¿Te interesa alguno?"
n8n manda esto al cliente vía Evolution API.
Paso 7: El huésped confirma
"Perfecto, el Cafetal."
n8n → Orchestrator → nodo booking → tool crear_reserva:
POST http://travel_backend:8001/api/v1/reservations/public
Header: X-Internal-Token: $INTERNAL_API_KEY
Body:
{
"property_id": "kasiri-01",
"guest_phone": "+573001234567",
"guest_name": "Juan Pérez", // si el LLM lo extrajo (con cleanup contra alucinaciones)
"room_type_id": "kasiri-cafetal",
"check_in": "2026-05-15",
"check_out": "2026-05-18",
"guests_count": 2
}
El reservation_service.create_reservation_public():
1. Idempotency guard: busca si ya existe reserva activa para (guest_id, room_type_id, check_in, check_out). Si existe → la devuelve sin INSERT nuevo (anti-doble-click).
2. Crea o reutiliza Guest por phone.
3. Encuentra room disponible del room_type (algoritmo _find_available_room_id).
4. Calcula precio con Season multiplier.
5. INSERT Reservation con código único KASIRI-A1B2C3.
Respuesta:
{
"code": "KASIRI-A1B2C3",
"total_amount": 621000,
"status": "confirmed",
"guest": {"name": "Juan Pérez", "phone": "+573001234567"},
"check_in": "2026-05-15",
"check_out": "2026-05-18"
}
Paso 8: Confirmación al huésped
"¡Reserva confirmada! ✅ Código: KASIRI-A1B2C3 Total: $621.000 (3 noches × Domo Cafetal) Te enviamos instrucciones de pago por separado."
Paso 9: Recepcionista verifica en el dashboard
El recepcionista abre Travel Frontend (dashboard PMS) en travel.timeliber.com.co:
1. Login con JWT (POST /auth/login).
2. Lista de reservas (GET /reservations?property_id=kasiri-01&page=1).
3. Ve la reserva nueva KASIRI-A1B2C3 con todos los datos.
Esta vista usa paginación Page[T] y eager loading (joinedload(Reservation.guest)) — no hay N+1.
Paso 10: El huésped consulta más tarde
Desde un canal cualquiera (WhatsApp, formulario web), el huésped puede preguntar por su reserva:
Sin JWT — pero requiere verificador (email o guest_phone) que coincida con el Guest.
Si el verificador no coincide → 404 (mensaje genérico, no leak de existencia — OWASP API1).
Lo que aprendiste
| Concepto | Dónde se aplicó |
|---|---|
| Multi-tenant aislado | Todo filtra por property_id |
| Idempotencia | Paso 7 — no se duplican reservas |
| Precios estacionales | Paso 5 — multiplicador automático |
| Eager loading | Paso 9 — sin N+1 |
| OWASP API1 (BOLA) | Paso 10 — verificador genérico |
| Soft delete | Implícito en queries (filtro deleted_at IS NULL) |
| Tokens internos vs JWT | X-Internal-Token (M2M Orchestrator), JWT (humano) |
Próximos pasos
- Si quieres entender los detalles del backend → ../../backend/docs/04_EXPLANATION/ARCHITECTURE.md
- Si quieres tocar la API → ../../backend/docs/03_REFERENCE/endpoints.md
- Si vas a añadir un nuevo hotel → add-new-tenant.md
- Si algo no funciona → TROUBLESHOOTING.md