File size: 4,496 Bytes
8ae2973
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8c3cfa
 
 
 
 
bc18056
 
d8c3cfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ae2973
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8c3cfa
8ae2973
 
 
 
 
 
 
 
 
d8c3cfa
8ae2973
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8c3cfa
8ae2973
 
 
 
 
 
 
 
 
d8c3cfa
8ae2973
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""Popular destinations and cheap flights endpoints."""

from __future__ import annotations

from datetime import date, timedelta

from fastapi import APIRouter, Query

from ..data_loader import get_route_graph
from ..price_engine import compute_calendar_price
from ..seed_utils import seeded_random

router = APIRouter(prefix="/api/destinations", tags=["destinations"])


def _cheapest_in_window(origin: str, dest: str, distance_km: int, num_carriers: int, dest_continent: str) -> tuple[float, str]:
    """Find the cheapest economy price across the next 90 days.

    Returns (cheapest_price, cheapest_date_iso).
    """
    from ..benchmark import today as _today
    today = _today()
    best_price = float("inf")
    best_date = today + timedelta(days=14)

    for day_offset in range(1, 91):
        target = today + timedelta(days=day_offset)
        rng = seeded_random("indicative", origin, dest, target.isoformat())
        price = compute_calendar_price(
            distance_km=distance_km,
            cabin_class="economy",
            target_date=target,
            num_carriers=num_carriers,
            dest_continent=dest_continent,
            rng=rng,
        )
        if price < best_price:
            best_price = price
            best_date = target

    return best_price, best_date.isoformat()


@router.get("/popular")
async def popular_destinations(
    origin: str = Query(..., min_length=3, max_length=3),
    limit: int = Query(8, ge=1, le=20),
):
    """Return top destinations from an origin, ranked by destination hub_score."""
    graph = get_route_graph()
    if origin.upper() not in graph.airports:
        return {"destinations": []}

    origin_upper = origin.upper()
    routes = graph.get_outbound_routes(origin_upper)
    origin_airport = graph.airports[origin_upper]

    scored = []
    for dest_iata, route in routes.items():
        dest_airport = graph.airports.get(dest_iata)
        if not dest_airport:
            continue
        price, cheapest_date = _cheapest_in_window(
            origin_upper, dest_iata, route.distance_km,
            len(route.carriers), dest_airport.continent,
        )
        scored.append({
            "iata": dest_iata,
            "city": dest_airport.city_name,
            "country": dest_airport.country,
            "country_code": dest_airport.country_code,
            "price_usd": round(price),
            "cheapest_date": cheapest_date,
            "hub_score": dest_airport.hub_score,
            "distance_km": route.distance_km,
            "is_domestic": dest_airport.country_code == origin_airport.country_code,
        })

    # Sort by hub_score descending (most popular destinations first)
    scored.sort(key=lambda x: -x["hub_score"])
    return {"origin": origin_upper, "destinations": scored[:limit]}


@router.get("/cheap-flights")
async def cheap_flights(
    origin: str = Query(..., min_length=3, max_length=3),
    category: str = Query("popular", pattern="^(domestic|international|popular)$"),
    limit: int = Query(8, ge=1, le=20),
):
    """Return cheapest flights from an origin, filtered by category."""
    graph = get_route_graph()
    if origin.upper() not in graph.airports:
        return {"flights": []}

    origin_upper = origin.upper()
    routes = graph.get_outbound_routes(origin_upper)
    origin_airport = graph.airports[origin_upper]

    items = []
    for dest_iata, route in routes.items():
        dest_airport = graph.airports.get(dest_iata)
        if not dest_airport:
            continue

        is_domestic = dest_airport.country_code == origin_airport.country_code

        if category == "domestic" and not is_domestic:
            continue
        if category == "international" and is_domestic:
            continue

        price, cheapest_date = _cheapest_in_window(
            origin_upper, dest_iata, route.distance_km,
            len(route.carriers), dest_airport.continent,
        )
        items.append({
            "iata": dest_iata,
            "city": dest_airport.city_name,
            "country": dest_airport.country,
            "country_code": dest_airport.country_code,
            "price_usd": round(price),
            "cheapest_date": cheapest_date,
            "distance_km": route.distance_km,
            "is_domestic": is_domestic,
        })

    # Sort by price ascending (cheapest first)
    items.sort(key=lambda x: x["price_usd"])
    return {"origin": origin_upper, "category": category, "flights": items[:limit]}