# Humming Airways — Motor de Reservas

Documentación de los conceptos centrales del motor de reservas aéreas.
Código fuente: `intranet/includes/fns/airlines.php` (prefijo de funciones: `AL_`).

---

## Jerarquía de conceptos

Las entidades tienen dependencias estrictas: no puede existir una sin que exista la anterior.

```
airport (Aeropuerto)   ← prerequisito para segmentos (airportFrom / airportTo)

route (Ruta)           ← prerequisito para flights
└── flight (Vuelo)     ← requiere routeId
    │
    ├── [plan_setup]   ← programación del vuelo; contenida en flight + sus segments
    │     └── segment (Segmento Maestro) ← requiere flightId + airportFrom + airportTo
    │           segment define cada tramo: AER→VDM, VDM→PMY, etc.
    │           Un plan_setup = flight + todos sus segments
    │
    └── plan (Instancia de Vuelo) ← requiere flightId     [flight_instance]
          plan = una fecha concreta de ejecución del flight
          └── leg (Tramo de Ejecución) ← materialización de cada segment para ese plan
              │
              ├── booking (Expediente de Compra) ─── reservation (1 por pasajero × tripType)
              │     ├── passengers                        1 pasajero, ida+vuelta = 2 reservations
              │     ├── payment                           2 pasajeros, ida+vuelta = 4 reservations
              │     ├── baggage (transactions)
              │     ├── fare / special_fare
              │     └── voucher (opcional)
              │
              └── itinerary ← 1 fila por reservation × leg recorrido
                    Ejemplo: 4 reservations × 2 legs (escala) = 8 filas en fly.itinerary
```

**Reglas de dependencia:**
1. Un `plan` no puede existir sin un `flightId`.
2. Un `flight` no puede existir sin un `routeId`.
3. Un `segment` (y por tanto un `plan_setup` completo) no puede existir sin los `airportId` de `airportFrom` y `airportTo`.
4. Una `reservation` no puede iniciarse sin un `planId`.
5. `itinerary` se crea *después* de la `reservation` — vincula la reserva existente con los legs del plan.

**Relación plan_setup ↔ segment:**
`plan_setup` no es una tabla independiente — es un concepto que agrupa un `flight` junto con todos sus `segments`. Los segments son los componentes que definen la estructura interna del plan_setup: cada segment es un tramo del vuelo regular (con horario, duración, escala y vigencia).

---

## Conceptos

### plan_setup (Programación de Vuelos — Schedule)

Define la planificación general de los vuelos de una aerolínea. Es la agenda o el horario maestro que establece qué vuelos se operarán durante un período (por ejemplo, una temporada), incluyendo sus horarios, rutas, frecuencias (días de la semana o fechas) y la asignación del tipo de aeronave o capacidad necesaria.

No son vuelos individuales y específicos listos para ser operados, sino el conjunto de **reglas y previsiones** para generar esos vuelos.

---

### flight_instance (Instancia de Vuelo Programada)

Representa la ejecución específica y concreta de un vuelo que forma parte de la planificación general (plan_setup). Es un vuelo individual y único que se ha generado a partir del schedule para una fecha y hora específicas, con detalles como la aeronave particular asignada (matrícula), los horarios precisos y su estado operativo (programado, retrasado, en vuelo, cancelado, etc.).

Es el vuelo tal como se presenta en los sistemas de operación y venta para una fecha determinada.

---

### route (Ruta)

Representa la conexión operativa entre dos o más aeropuertos que la aerolínea opera como unidad de negocio. No define horarios ni fechas, sino el **corredor geográfico** (ej: `AER → VDM`) al que pertenecen uno o más vuelos.

- Cada vuelo (`flight`) está asociado a una ruta mediante `routeId`.
- Una misma ruta puede tener múltiples vuelos con distintas frecuencias o configuraciones de segmentos.

**Tablas:** `fly.routes`, `fly.flights` (via `routeId`)

---

### segment (Segmento Maestro)

Es el tramo físico elemental de un vuelo tal como está definido en el **plan_setup**. Un vuelo multi-escala tiene múltiples segmentos (ej: `AER → VDM → PMY`).

Cada segmento define:
- `airportFrom` / `airportTo` — aeropuertos de origen y destino
- `departureTime` — hora de salida
- `flightDuration` — duración del tramo en minutos
- `stopoverTime` — tiempo de escala en destino antes del siguiente tramo
- `dayOfWeek` / `periodicity` — días de la semana en que opera
- `valid_from` / `valid_to` — vigencia del segmento

Los segmentos tienen **vigencias** para modelar cambios de horario a lo largo del tiempo sin perder historia. Cuando se modifica el horario, se crea un nuevo segmento con distinto `valid_from`; el anterior cierra su `valid_to` automáticamente (`AL_SEGMENT_FIX_VALIDITY`).

**Tabla:** `fly.segments`

---

### leg (Tramo de Ejecución)

Es la **materialización concreta** de un segmento dentro de una `flight_instance` (`plan`) específica. Donde el segmento es la regla maestra del `plan_setup`, el leg es el tramo real ejecutado en una fecha determinada, con el avión asignado y los pasajeros embarcados.

- `fly.legs` es la **fuente de verdad** para calcular ocupación real, check-in y operaciones DCS.
- Una reserva puede aparecer en múltiples legs cuando el vuelo tiene escalas (vuelo multi-leg o circular), lo cual se registra en `fly.itinerary`.

Columnas clave: `leg_id`, `planId`, `leg_order`, `airportFrom`, `airportTo`, `departureTime` (DATETIME), `arrivalTime` (DATETIME), `flightDuration`, `stopoverTime`.

**Tabla:** `fly.legs`

---

### Regla: cuándo usar `fly.segments` vs `fly.legs`

Esta distinción es crítica para cualquier query operacional. Usarlas incorrectamente produce datos del template en vez de datos del plan real.

#### `fly.segments` — solo para setup y configuración

| Uso correcto | Función/contexto |
|---|---|
| Crear / editar / eliminar tramos del plan_setup | `AL_SEGMENT_SETUP()`, `AL_UPDATE_FLIGHT_SCHEDULE()` |
| Gestionar vigencias (`valid_from` / `valid_to`) | `AL_SEGMENT_FIX_VALIDITY()` |
| Verificar conectividad de rutas configuradas | `AL_CONNECTION_IS_POSSIBLE()` |
| Asociar tarifas a conexiones (fares / specialfares) | queries de tarificación |
| Generar legs desde el template | `AL_RECREATE_LEGS_HISTORY()` |
| Consultar metadatos del vuelo (`dayOfWeek`, `greatCircleDistance`) | admin y reportes de schedule |

#### `fly.legs` — para todo lo operacional

| Uso correcto | Contexto |
|---|---|
| Disponibilidad de asientos para una fecha | `AL_AVAILABLES()` |
| Horarios de salida/llegada de un plan concreto | `AL_FLIGHT_STATICS()`, `AL_SECTION_INFO()` |
| Aeropuertos activos disponibles para búsqueda | `AL_COMBINED_AIRPORTS()` |
| Lista de vuelos restringidos para una fecha | `AL_RESTRICTED()` |
| Reconstruir itinerario de un plan | `AL_PLAN_REBUILD()` |
| Cualquier query que reciba `planId` o `flightDate` concreto | — |

#### Patrón de join correcto: itinerary → legs

```sql
-- Correcto: unir itinerary con legs via planId + airportFrom/To
join fly.legs l on l.planId = plans.planId
    and l.airportFrom = i.airportFrom
    and l.airportTo   = i.airportTo

-- Incorrecto: unir itinerary con segments via flightId + vigencia
join fly.segments s on s.flightId = i.flightId
    and s.airportFrom = i.airportFrom
    and flightDate between s.valid_from and s.valid_to
```

#### Por qué importa

- `fly.segments` filtra por `valid_from...valid_to` (rango de vigencia del template). Si el horario cambió durante la vida del plan, o si el vuelo es privado/especial, el template no refleja la realidad del plan.
- `fly.legs` siempre corresponde al plan concreto — es lo que realmente se voló o se va a volar.
- `count(distinct r.reservationId)` **subestima** la ocupación en vuelos multi-leg. La forma correcta es `count(distinct i.id)` sobre `fly.itinerary`.

---

### reservation (Reserva Individual) y booking (Expediente de Compra)

`reservation` y `booking` son conceptos del mismo nivel: el **acto de compra**. Están ligados por `bookingId`.

---

**booking** es el expediente de compra: agrupa toda la transacción, el pago, el estado del proceso y los datos del comprador.

- `bookingId` — identificador interno
- `reservationCode` — código alfanumérico visible para el pasajero
- `bookingStatus` — **bitmask** que acumula los hitos del proceso (ver tabla abajo)
- El estado se registra históricamente en `fly.blog` para trazabilidad completa
- Un booking tiene siempre **un único pago aprobado** (`payment_status IN ('approved','redeemed')`)

---

**reservation** es el ticket vendido: un registro por pasajero por dirección de viaje (`tripType`). Registra la conexión comercial vendida (los extremos del viaje), no los tramos intermedios.

Campos clave:
- `tripType` — `OutBound = 0` (ida), `InBound = 1` (vuelta)
- `sectionFrom` / `sectionTo` — aeropuertos de los extremos vendidos (ej: Buenos Aires → Villa María)
- `fare_options` — bitmask con el tipo de pasajero y clase de servicio seleccionados

**Ejemplo:** 2 pasajeros, ida y vuelta a Villa María → **4 registros** en `fly.reservations`:

| reservationId | passengerId | tripType | sectionFrom | sectionTo |
|---|---|---|---|---|
| 1 | Pasajero A | OutBound (0) | Buenos Aires | Villa María |
| 2 | Pasajero B | OutBound (0) | Buenos Aires | Villa María |
| 3 | Pasajero A | InBound (1) | Villa María | Buenos Aires |
| 4 | Pasajero B | InBound (1) | Villa María | Buenos Aires |

Si el vuelo hace escala en Venado Tuerto, esas mismas 4 reservations generan **8 filas** en `fly.itinerary` (2 legs × 4 reservations), registrando cada asiento en cada tramo físico.

**Tablas:** `fly.reservations`, `fly.booking`, `fly.blog` (historial de estados booking)

#### Estados del booking (bitmask acumulativo)

| Constante | Valor | Descripción |
|---|---|---|
| `BOOKING_INITIATED` | 1 | Reserva iniciada (vuelo seleccionado) |
| `BOOKING_BAGGAGE` | 2 | Equipaje seleccionado |
| `BOOKING_PASSENGER` | 4 | Pasajeros cargados |
| `BOOKING_VERIFIED` | 8 | Usuario verificado |
| `BOOKING_ACCOUNT` | 16 | Cuenta creada o asociada |
| `BOOKING_CLOSED` | 32 | Reserva bloqueada (no editable) |
| `BOOKING_PAID` | 64 | Pago confirmado |
| `BOOKING_INVOICED` | 128 | Factura emitida |
| `BOOKING_REFUNDED` | 512 | Pago reembolsado |
| `BOOKING_EXPIRED` | 8192 | Expirada por inactividad |
| `BOOKING_EMAILED` | 65536 | Confirmación enviada por email |

`BOOKING_NEGATIVE_CONDITION = BOOKING_EXPIRED | BOOKING_REFUNDED` — máscara para filtrar reservas en estado negativo.

**Tablas:** `fly.booking`, `fly.blog` (historial de estados)

---

### itinerary (Itinerario de Reserva)

Es la tabla de **vinculación entre reservas y legs**. Cada fila indica que un pasajero (via `reservationId`) recorre un tramo concreto (`legId`) de una `flight_instance`.

- Para vuelos directos: cada reserva genera **una fila**.
- Para vuelos con escala: genera **una fila por cada leg** que el pasajero recorre.

**Ejemplo:** 2 pasajeros, ida y vuelta, vuelo con escala en Venado Tuerto:
- `fly.reservations` → 4 registros (1 por pasajero × tripType)
- `fly.itinerary` → 8 registros (4 reservations × 2 legs: BsAs→VT y VT→VillaMaría)

Esta estructura es la fuente correcta para calcular la **ocupación real** de un plan:

```sql
-- CORRECTO: count(i.id) sin DISTINCT
SELECT r.planId, count(i.id) AS reservedSeats
FROM fly.itinerary i
JOIN fly.reservations r ON r.reservationId = i.reservationId
JOIN fly.payments p ON p.bookingId = r.bookingId
WHERE p.payment_status IN ('approved', 'redeemed')
GROUP BY r.planId;

-- INCORRECTO para multi-leg: subestima la ocupación
-- count(DISTINCT reservationId)
```

El **factor de ocupación** se calcula como:
```
reserveFactor = reservedSeats / (totalCapacity × cantidadDeLegs)
```

**Tabla:** `fly.itinerary`

---

### fare / special_fare (Tarifa)

Define la estructura de precios aplicable a una reserva. Es un JSON (`fare_config`) con tres bloques:

```json
{
  "Passengers": {
    "ADT": { "base_fare": 150, "age_range": "16-100" },
    "CHD": { "base_fare": 100, "age_range": "2-11" },
    "INF": { "base_fare": 0,   "age_range": "0-1" },
    "SNR": { "base_fare": 120, "age_range": "60-100" }
  },
  "Baggage": {
    "hand_bag":    { "unit_price": 0,  "is_included": 1, "max_weight": 5  },
    "carry_on":    { "unit_price": 30, "is_included": 0, "max_weight": 10 },
    "checked_bag": { "unit_price": 50, "is_included": 0, "max_weight": 23 }
  },
  "Policies": {
    "visible_in_search": 1,
    "booking_enabled": 1,
    "voucher_capacity_ratio": 0,
    "voucher_min_advance_days": 9,
    "voucher_max_per_booking": 0
  }
}
```

**Jerarquía de precedencia (la más específica gana):**

```
special_fare (connection1 + connection2 + fromDate)  ← mayor precedencia
    └── fare base (routeId + fromDate)               ← fallback
```

- IVA por defecto: **10.5%** (`AIRLINE_DEFAULT_TAX_PERCENT`)
- Moneda base: **USD** (conversión a ARS via `mon_ctz` del booking)

**Funciones:** `AL_GET_FARES()`, `AL_SETUP_SEPECIAL_FARE()`, `AL_INSERT_NEW_FARE()`
**Tablas:** `fly.fares` (base), `fly.specialfares` (especial)

---

### voucher (Voucher de Viaje)

Instrumento prepago que **reemplaza el pago monetario** en la reserva. Está vinculado a una cuenta (`accId`) y a una conexión específica (`connection1` / `connection2`).

#### Tipos de voucher

| Constante | Valor | Descripción |
|---|---|---|
| `VOUCHER_FLEXIBLE` | 1 | Cantidad de viajes válidos dentro de un período libre |
| `VOUCHER_MONTHLY` | 2 | `tripCount × voucherMonths` viajes en el período |
| `VOUCHER_FIXED` | 4 | Válido para una fecha específica |

#### Ciclo de vida

```
Creación (AL_ADD_NEW_VOUCHER)
  ↓  genera voucherCode único
Asignación a reserva (payment_method = 'voucher')
  ↓  registra en fly.redemptions (reservationId → voucherId)
Consolidación (AL_VOUCHERS_UPDATE_USED)
  ↓  recalcula tripsUsed
Pre-facturación (AL_PREFACT)
  ↓  genera ítems para facturación en xGestion
```

#### Regla de consumo especial — `burnPerFlight` (regla 92438, Villa María)

```
Si burnPerFlight > 0:
  tripsUsed = burnPerFlight × cantidad_de_planes_operados_en_período
  (los viajes se consumen por frecuencia operada, aunque no haya reservas)

Si burnPerFlight = 0 (default):
  tripsUsed = cantidad de redenciones efectivas en fly.redemptions
```

**Tablas:** `fly.vouchers`, `fly.redemptions`

---

### passenger (Pasajero)

Es **quien viaja**. Sus datos (nombre, apellido, DNI, fecha de nacimiento, nacionalidad) se guardan en `fly.passengers` y se asocian a una `reservation`.

En algunos casos se carga un **pasajero fantasma** (`J. Doe`) como placeholder cuando los datos reales no están disponibles al momento de la reserva. El pasajero fantasma se completa manualmente antes del vuelo.

Un mismo `passengerId` puede aparecer en múltiples reservas (viajero frecuente).

**Tabla:** `fly.passengers`

---

### user (Usuario Operador)

Es **quien realiza la operación de compra**. Puede ser:

- **Web pública:** el comprador final (generalmente quien paga). Se registra y verifica por email.
- **Intranet:** el operador de xGestion que carga la reserva en nombre de un cliente.

El `userId` queda registrado en el `booking` como responsable de la transacción. No es necesariamente el mismo que el pasajero ni el titular de la cuenta a facturar.

**Tabla:** `fly.users`

---

### account (Cuenta de Facturación)

Es **a quién se le factura**. Puede ser:

- La misma persona que el `user` (compra individual — misma persona, distinto ID).
- Una **cuenta corporativa** (empresa cliente con vouchers propios o crédito).
- La cuenta genérica **"Consumidor Final"** (`accId = 131`) para compras sin cuenta asociada.

El `accId` en el `booking` determina la cuenta de facturación. Es también el nexo con los `vouchers`: cada voucher pertenece a un `accId` específico.

**Relación con xGestion:** `fly.accounts` no es una tabla independiente del motor — comparte los mismos IDs que `db_139.cuentas` (la base de datos de Humming en xGestion). El sistema xGestion es multi-empresa: cada empresa tiene su propia DB cuyo nombre es su ID en `system.sistemas`. Humming tiene el ID `139`, por lo que su base de datos de negocio es `db_139`. `fly.*` es una extensión propia de Humming dentro de ese ecosistema.

El `accId = 131` ("Consumidor Final") es un ID reservado por convención en **todas** las bases de datos de xGestion para representar compras anónimas o sin cuenta corporativa asignada.

**Tabla:** `fly.accounts` (IDs equivalentes a `db_139.cuentas`)

---

### payment (Pago)

Es **la forma en que se cancela el booking**. Un booking tiene un único pago aprobado.

Métodos posibles:
- `account` — débito contra la cuenta (`accId`), sin pasarela externa
- `voucher` — redención de un voucher de viaje (`fly.redemptions`)
- `uala` — pasarela de pago externa (genera `uuid` y se sincroniza con `system.uala`)

Estados: `pending` → `approved` / `refunded`

El `payment_status IN ('approved', 'redeemed')` es la condición que determina si una reserva cuenta como ocupación real.

**Tabla:** `fly.payments`

---

## Tipos de pasajero (códigos IATA)

| Código | Constante | Bitmask | Edad |
|---|---|---|---|
| `ADT` | `passengerTypeAdult` | 1 | 16–100 años |
| `CHD` | `passengerTypeChild` | 2 | 2–11 años |
| `INF` | `passengerTypeInfant` | 4 | 0–1 año (sin asiento) |
| `SNR` | `passengerTypeSenior` | 8 | 60+ años |

---

## Tipos de equipaje

| Código | Constante | Bitmask | Peso máx | Precio | Incluido |
|---|---|---|---|---|---|
| `hand_bag` | `baggageHandBag` | 128 | 5 kg | — | Sí |
| `carry_on` | `baggageCarryOn` | 256 | 10 kg | USD 30 | No |
| `checked_bag` | `baggageChecked` | 512 | 23 kg | USD 50 | No |

---

## Flujo principal de una reserva

```
1. AL_AIRPORTS_FROM()         → Aeropuertos origen disponibles
2. AL_TRIP()                  → Destinos posibles desde origen
3. AL_SECHEDULE()             → Vuelos disponibles en rango de fechas
   ├── AL_GET_FLIGHTS()           ← consulta fly.plans + fly.legs
   ├── AL_GET_FARES()             ← tarifa especial o base
   ├── AL_FLIGHT_DATE_LIMITS()    ← valida ventana de venta
   └── AL_AVAILABLES()            ← asientos reales (descuenta cupo voucher)
4. AL_SETUP_BOOKING_PASSENGERS() → Carga pasajeros → fly.passengers + fly.reservations + fly.itinerary
5. AL_UPDATE_BOOKIG_BAGAGGE()    → Equipaje → fly.transactions
6. AL_USER_REGISTER/VERIFY()     → Verificación de usuario
7. AL_UPDATE_BOOKIG_ACCOUNT()    → Asocia cuenta (accId)
8. AL_BOOKING_PRICES()           → Calcula tarifas + equipaje + descuentos + IVA
9. AL_PROMOTIONS()               → Aplica mejor promo disponible → fly.promox
10. AL_PAYMENT_REGISTER()        → Registra pago → fly.payments
11. AL_SEND_BOOKING_CONFIRM()    → Envía email de confirmación
12. AL_BOOKIG_ADD_LOG()          → Registra hito en fly.blog
```
