Saltar a contenido

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-01 está 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.

WhatsApp → Evolution API → n8n webhook

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:

GET /api/v1/reservations/by-code/KASIRI-A1B2C3?guest_phone=+573001234567

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