Documentación técnica · generado el 18/10/2026
Full Vuelos es una aplicación de práctica del campus Full Advanced: una aerolínea de bajo costo (identidad visual inspirada en Wingo) que sirve como objetivo realista para practicar testing manual y automatizado.
Esta sección documenta las reglas de negocio exactas que implementa el código — es la referencia para escribir casos de prueba contra la app.
viajero.demo / Vuelos2026! con dos reservas (una cancelable a ~5 días, una dentro de la ventana de check-in)./reservar (GET) redirige (303) a la página principal con un aviso amable en lugar de mostrar un error crudo.Full Vuelos tiene su propia tabla de usuarios (vuelos_users) y su propia cookie de sesión (fv_sesion, firmada, válida 14 días y limitada a /practica-vuelos). No hay relación con las cuentas del campus Full Advanced:
POST /practica-vuelos/registro crea una cuenta y abre sesión de inmediato, redirigiendo a la página principal con el aviso de bienvenida. Si el inventario de vuelos existe, la cuenta recibe las mismas dos reservas de ejemplo que viajero.demo (seed_example_bookings): una ida y vuelta BOG ↔ BAQ cancelable (tarifa Plus + maleta documentada + selección de asiento, ~5 días) y un solo-trayecto BOG → MDE dentro de la ventana de check-in. Vuelos agotados o inventario ausente simplemente omiten la reserva correspondiente — el registro nunca falla por esto.
| Regla | Mensaje |
|---|---|
| Todos los campos obligatorios | Todos los campos son obligatorios. |
| Contraseña de mínimo 8 caracteres | La contraseña debe tener al menos 8 caracteres. |
| Usuario único | El nombre de usuario ya está registrado. |
| Correo único | El correo electrónico ya está registrado. |
Las contraseñas se almacenan con hash Argon2id.
POST /practica-vuelos/login. Cualquier fallo (usuario inexistente, contraseña errada, cuenta desactivada) responde 401 con el mismo mensaje genérico Usuario o contraseña inválidos. — nunca se revela cuál parte falló.
El parámetro next redirige tras el login, pero solo a rutas locales de /practica-vuelos; cualquier otro valor cae a la página principal (anti open-redirect).
Requieren sesión de Full Vuelos: el flujo de reserva (/reservar*), Mis reservas, el detalle de reserva, cancelación y check-in. Un visitante anónimo es redirigido (303) al login con ?next= apuntando a su destino original, query string incluida — la búsqueda y el manual son públicos.
POST /practica-vuelos/reservas/{codigo}/cancelar.
Reglas:
La reserva ya está cancelada.Solo puedes cancelar hasta 24 horas antes del primer vuelo.Efectos de una cancelación exitosa:
cancelled (definitivo; no hay reactivación).seats_total), en la misma transacción con bloqueo de filas.GET|POST /practica-vuelos/reservas/{codigo}/asientos.
can_select_seats): reserva confirmada, sin check-in, primer vuelo en el futuro y (tarifa Full en algún tramo o extra seat_selection comprado o ventana de check-in abierta — entonces es gratis).El asiento X no existe.), no repetido dentro de la misma selección (El asiento X está repetido.), no ocupado por otra reserva del mismo vuelo (El asiento X ya está ocupado.).La selección de asiento está incluida en la tarifa Full, se puede comprar como extra, o es gratis durante el check-in (24 horas antes del vuelo).POST /practica-vuelos/reservas/{codigo}/checkin (también listado en /checkin con las reservas elegibles).
Reglas:
[salida del primer vuelo − 24 h, salida). Antes: El check-in abre 24 horas antes del primer vuelo y cierra a la hora de salida.Esta reserva ya hizo el check-in.Efectos del check-in:
checked_in_at queda marcado.1A, 1B, …, letras A–F), saltando los asientos ya ocupados por otros pasajeros del mismo vuelo. La asignación es determinística y nunca repite asientos. Los bebés quedan sin asiento (En regazo).| Estado | Etiqueta | Acciones |
|---|---|---|
| Confirmada, > 24 h, con derecho a asientos | Confirmada |
Elegir asientos, Cancelar |
| Confirmada, > 24 h, sin derecho a asientos | Confirmada |
Cancelar |
| Confirmada, dentro de 24 h | Confirmada |
Elegir asientos (gratis), Hacer check-in |
| Check-in hecho | Check-in hecho + fecha |
Ninguna |
| Cancelada | Cancelada |
Ninguna |
El paso de pago no procesa cargos reales: no hay pasarela, no hay red bancaria. Aun así valida como un formulario real, porque el propósito de la aplicación es practicar pruebas contra reglas verídicas.
| Campo | Regla | Mensaje |
|---|---|---|
| Número | Solo dígitos, 13–19 caracteres, algoritmo de Luhn | El número de tarjeta no es válido. |
| Titular | Obligatorio | Indica el nombre del titular de la tarjeta. |
| Vencimiento | Formato MM/AA, mes 01–12, fecha futura |
La fecha de vencimiento debe ser MM/AA. / La tarjeta está vencida. |
| CVV | 3 o 4 dígitos | El CVV debe tener 3 o 4 dígitos. |
Tarjeta de prueba canónica de la demo: 4111 1111 1111 1111 con cualquier vencimiento futuro y cualquier CVV válido.
card_last4) en la reserva, para mostrar ···· 1111 en el detalle.El vuelo X no tiene suficientes asientos disponibles. y no se cobra nada.Una reserva es un registro con código PNR de 6 caracteres del alfabeto sin ambigüedades ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (sin 0/O ni 1/I), generado al azar hasta ser único. Un viaje de ida y vuelta es una sola reserva con dos tramos (vuelos_booking_legs, posición 0 = ida, 1 = regreso).
Todas se verifican dos veces: al cotizar (paso de pago) y dentro de la transacción (paso de confirmación). Cualquier violación responde 400 con el mensaje correspondiente:
| Regla | Mensaje |
|---|---|
| 1–9 personas por reservación | El grupo máximo permitido son 9 personas por reservación. |
| Al menos un adulto | Debe viajar al menos un adulto. |
| Máximo 1 bebé por adulto | Cada bebé debe viajar en el regazo de un adulto: máximo 1 bebé por adulto. |
Tipo de pasajero válido (adulto / menor / bebe) |
El tipo del pasajero N no es válido (adulto, menor o bebe). |
| Nombre, apellido y documento obligatorios por pasajero | Completa nombre, apellido y documento del pasajero N. |
| Documento único dentro de la reserva | El documento X está repetido en la reserva. |
| El vuelo de regreso debe ser la ruta inversa al de ida | El vuelo de regreso debe ser la ruta inversa al de ida. |
Tarifa válida (basica / plus / full) |
Tarifa desconocida: X |
| El vuelo debe estar programado | El vuelo X ya no está disponible. |
| El vuelo no debe haber partido | El vuelo X ya partió. |
| Asientos suficientes para los pasajeros con silla | El vuelo X no tiene suficientes asientos disponibles. |
seats_total (180) y seats_available.seats_available se descuenta en el conteo con asiento (adultos + menores; los bebés en regazo no descuentan) por tramo, en la misma transacción que crea reserva + tramos + pasajeros: o todo o nada.with_for_update en orden de id (mismo idioma anti-deadlock que las transferencias del Banco Americano): dos compras simultáneas del último asiento no pueden ganar ambas.seats_available >= pasajeros_con_asiento, así que el agotamiento solo se manifiesta entre la búsqueda y el pago (condición de carrera clásica — ejercicio de testing intencional).validate_search impone: origen dentro del mapa direccional de venta (ROUTE_DESTINATIONS: BAQ, CCS, CLO, GUA, HAV), destino servido por ese origen, ida no en el pasado, regreso ≥ ida (si aplica), al menos 1 adulto, ≤ 1 bebé por adulto y ≤ 9 personas en total. Los resultados se ordenan por hora de salida y solo incluyen vuelos del día consultado con estado scheduled.
Los selectores de ciudad se ordenan alfabéticamente por nombre de ciudad (ignorando acentos), no por código IATA.
date_strip calcula, para la ruta y el día consultados, la disponibilidad de los 3 días anteriores y 3 posteriores (nunca en el pasado): cada día sabe si hay vuelos con asientos suficientes y el precio base más barato. La UI pinta de gris y deshabilita los días sin vuelos (title="No hay vuelos disponibles") y enlaza los disponibles a /buscar conservando ruta, tipo de viaje y composición de pasajeros. Cuando no hay selección posible, el botón Continuar se deshabilita y GET /reservar con selección inválida redirige a la página principal con ?aviso=seleccion.
filter_bookings aplica búsqueda por código (substring, sin mayúsculas) y filtro de estado (todas / confirmada / checkin / cancelada); split_bookings separa el resultado en próximas (confirmadas con primer vuelo futuro, más cercanas primero) e historial (voladas o canceladas, más recientes primero, plegadas en la UI).
Cada vuelo tiene un precio base (USD, tarifa Básica, por pasajero). Las tarifas superiores multiplican el precio base y agregan inclusiones:
| Tarifa | Multiplicador | Incluye |
|---|---|---|
Básica (basica) |
×1.00 | Artículo personal |
Plus (plus) |
×1.25 | + Equipaje de mano 10 kg |
Full (full) |
×1.50 | + Equipaje de mano, maleta documentada 20 kg y selección de asiento |
La tarifa se elige una vez por reserva y aplica a todos los tramos (ida y regreso).
Cada pasajero tiene un tipo (pax_type):
| Tipo | Edad | Precio | Asiento |
|---|---|---|---|
adulto |
12+ años | Tarifa plena | Sí |
menor |
2-11 años | Tarifa plena | Sí |
bebe |
0-23 meses | Gratis (en regazo) | No |
Reglas del grupo (validadas en búsqueda y en la reserva):
Debe viajar al menos un adulto.Cada bebé debe viajar en el regazo de un adulto: máximo 1 bebé por adulto.El grupo máximo permitido son 9 personas por reservación.Los cálculos de precio e inventario usan el conteo con asiento (seated = adultos + menores): los bebés no pagan ni descuentan inventario.
Los extras se compran por separado y aplican a todos los pasajeros con asiento de la reserva (regla deliberada de simplificación del demo):
| Extra | Clave | Precio por pasajero |
|---|---|---|
| Equipaje de mano 10 kg | carry_on |
$18.00 |
| Maleta documentada 20 kg | checked_bag |
$35.00 |
| Selección de asiento | seat_selection |
$9.00 |
Regla de inclusión: un extra que la tarifa ya incluye no se puede comprar — la UI no lo muestra y el servicio rechaza la compra con La tarifa <tarifa> ya incluye este extra. Con tarifa Full no hay extras disponibles.
Los precios se almacenan en USD; el selector 💱 de la barra superior (cookie fv_currency, 180 días) solo cambia cómo se muestran — incluidos búsqueda, tarifas, extras, resumen de pago y detalle de reserva. Las tasas son fijas y ficticias (entorno de demostración, sin feed de mercado):
| Moneda | Código | Símbolo | Tasa por USD | Decimales |
|---|---|---|---|---|
| Pesos argentinos | ARS | $ | 950 | 0 |
| Pesos chilenos | CLP | $ | 920 | 0 |
| Pesos dominicanos | DOP | RD$ | 59 | 2 |
| Pesos mexicanos | MXN | $ | 18.5 | 2 |
| Pesos colombianos | COP | $ | 4.200 | 0 |
| Soles peruanos | PEN | S/ | 3.75 | 2 |
| Reales brasileños | BRL | R$ | 5.4 | 2 |
| Florines de Aruba | AWG | Afl. | 1.79 | 2 |
| Dólares estadounidenses | USD | US$ | 1 | 2 |
Un código desconocido en la cookie o en el parámetro cae a USD. Formato: US$ 49.99 para USD; $ 209.958 COP para el resto (código como sufijo).
total = (Σ precio_tarifa(tramo) + Σ extras) × pasajeros_con_asiento
precio_tarifa(tramo) = base_price(tramo) × multiplicador(tarifa), redondeado a 2 decimales half-up por tramo.BOG→BAQ (base $58.99) ida y vuelta, tarifa Plus, 2 adultos + 1 bebé, maleta documentada como extra: