Show Eurostar seat availability and no-prices notice
fetch_prices now returns {'price': ..., 'seats': ...} per departure.
Seat count (labelled "N at this price") is shown below the fare — it
reflects price-band depth rather than total remaining seats. A yellow
notice is shown when the API returns journeys but all prices are null
(tickets not yet on sale).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd37f0619b
commit
05eec29b7d
4 changed files with 38 additions and 17 deletions
21
app.py
21
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -95,6 +95,11 @@
|
|||
<strong>Warning:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if no_prices_note %}
|
||||
<div style="margin-top:1rem;padding:0.75rem 1rem;background:#fffbeb;border:1px solid #f6e05e;border-radius:4px;color:#744210">
|
||||
{{ no_prices_note }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if trips or unreachable_morning_services %}
|
||||
|
|
@ -164,6 +169,9 @@
|
|||
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
|
||||
{% if row.eurostar_price is not none %}
|
||||
£{{ row.eurostar_price }}
|
||||
{% if row.eurostar_seats is not none %}
|
||||
<br><span style="font-size:0.75rem;color:#718096">{{ row.eurostar_seats }} at this price</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="color:#718096">–</span>
|
||||
{% endif %}
|
||||
|
|
@ -197,12 +205,15 @@
|
|||
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
|
||||
{% if row.eurostar_price is not none %}
|
||||
<span style="color:#a0aec0">£{{ row.eurostar_price }}</span>
|
||||
{% if row.eurostar_seats is not none %}
|
||||
<br><span style="font-size:0.75rem;color:#a0aec0">{{ row.eurostar_seats }} at this price</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="color:#a0aec0">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.6rem 0.8rem;font-weight:600">
|
||||
<span title="No same-day Bristol connection" style="color:#a0aec0">Too early</span>
|
||||
<span title="No same-day Bristol connection" style="color:#a0aec0;white-space:nowrap">Too early</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue