diff --git a/app.py b/app.py index 04c2adc..0ca44c7 100644 --- a/app.py +++ b/app.py @@ -146,14 +146,18 @@ def results(slug, travel_date): trips = combine_trips(gwr_trains, eurostar_trains, travel_date, min_connection, max_connection) - # Annotate each trip with Eurostar Standard price and total cost + # Annotate each trip with Eurostar Standard price, seats, and total cost for trip in trips: - es_price = eurostar_prices.get(trip['depart_st_pancras']) + es = eurostar_prices.get(trip['depart_st_pancras'], {}) + es_price = es.get('price') trip['eurostar_price'] = es_price - if es_price is not None: - trip['total_price'] = trip['ticket_price'] + es_price - else: - trip['total_price'] = None + trip['eurostar_seats'] = es.get('seats') + trip['total_price'] = trip['ticket_price'] + es_price if es_price is not None else None + + # If the API returned journeys but every price is None, tickets aren't on sale yet + no_prices_note = None + if eurostar_prices and all(v.get('price') is None for v in eurostar_prices.values()): + no_prices_note = 'Eurostar prices not yet available — tickets may not be on sale yet.' unreachable_morning_services = find_unreachable_morning_eurostars( gwr_trains, @@ -163,7 +167,9 @@ def results(slug, travel_date): max_connection, ) for svc in unreachable_morning_services: - svc['eurostar_price'] = eurostar_prices.get(svc['depart_st_pancras']) + es = eurostar_prices.get(svc['depart_st_pancras'], {}) + svc['eurostar_price'] = es.get('price') + svc['eurostar_seats'] = es.get('seats') result_rows = sorted( [{'row_type': 'trip', **trip} for trip in trips] @@ -196,6 +202,7 @@ def results(slug, travel_date): eurostar_count=len(eurostar_trains), from_cache=from_cache, error=error, + no_prices_note=no_prices_note, eurostar_url=eurostar_url, rtt_url=rtt_url, rtt_bristol_url=rtt_bristol_url, diff --git a/scraper/eurostar.py b/scraper/eurostar.py index 4bbdfd9..f12f283 100644 --- a/scraper/eurostar.py +++ b/scraper/eurostar.py @@ -162,12 +162,13 @@ def _generate_cid() -> str: return 'SRCH-' + ''.join(random.choices(chars, k=22)) -def fetch_prices(destination: str, travel_date: str) -> dict[str, int | None]: +def fetch_prices(destination: str, travel_date: str) -> dict[str, dict]: """ - Return Eurostar Standard prices for every departure on travel_date. + Return Eurostar Standard price and seat availability for every departure on travel_date. - Result: {depart_st_pancras: price_gbp_int_or_None} - None means the class is sold out or unavailable for that departure. + Result: {depart_st_pancras: {'price': int_or_None, 'seats': int_or_None}} + price is None when unavailable/not yet on sale; seats is the number of + Standard seats currently available for sale. """ dest_id = DESTINATION_STATION_IDS[destination] headers = { @@ -196,16 +197,18 @@ def fetch_prices(destination: str, travel_date: str) -> dict[str, int | None]: resp = requests.post(_GATEWAY_URL, json=payload, headers=headers, timeout=20) resp.raise_for_status() data = resp.json() - prices: dict[str, int | None] = {} + prices: dict[str, dict] = {} journeys = data['data']['journeySearch']['outbound']['journeys'] for journey in journeys: dep = journey['timing']['departureTime'] price = None + seats = None for fare in journey['fares']: if fare['classOfService']['code'] == 'STANDARD': p = fare.get('prices') if p and p.get('displayPrice'): price = int(p['displayPrice']) + seats = fare.get('seats') break - prices[dep] = price + prices[dep] = {'price': price, 'seats': seats} return prices diff --git a/templates/results.html b/templates/results.html index 403d01d..634ede8 100644 --- a/templates/results.html +++ b/templates/results.html @@ -95,6 +95,11 @@ Warning: {{ error }} {% endif %} + {% if no_prices_note %} +
+ {{ no_prices_note }} +
+ {% endif %} {% if trips or unreachable_morning_services %} @@ -164,6 +169,9 @@ {% if row.eurostar_price is not none %} £{{ row.eurostar_price }} + {% if row.eurostar_seats is not none %} +
{{ row.eurostar_seats }} at this price + {% endif %} {% else %} {% endif %} @@ -197,12 +205,15 @@ {% if row.eurostar_price is not none %} £{{ row.eurostar_price }} + {% if row.eurostar_seats is not none %} +
{{ row.eurostar_seats }} at this price + {% endif %} {% else %} {% endif %} - Too early + Too early {% endif %} diff --git a/tests/test_app.py b/tests/test_app.py index d4de5c2..8806137 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -214,13 +214,13 @@ def test_results_shows_unreachable_morning_eurostar_services(monkeypatch): assert '2 Eurostar services unavailable from Bristol' in html assert '09:30' in html assert 'ES 9001' in html - assert 'Unavailable from Bristol' in html + assert 'Too early' in html assert html.index('09:30') < html.index('10:15') def test_results_shows_eurostar_price_and_total(monkeypatch): # 07:00 on Friday 2026-04-10 → Anytime £138.70 (weekday, 05:05–08:25 window) - _stub_data(monkeypatch, prices={'10:01': 59}) + _stub_data(monkeypatch, prices={'10:01': {'price': 59, 'seats': 42}}) client = _client() resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120') @@ -268,4 +268,4 @@ def test_results_can_show_only_unreachable_morning_services(monkeypatch): assert 'No valid journeys found.' not in html assert '1 Eurostar service unavailable from Bristol' in html assert '09:30' in html - assert 'Unavailable from Bristol' in html + assert 'Too early' in html