Parity logic:
- Extract _fetch_latest_alerts() and _upsert_alerts() helpers so the alert
upsert loop is no longer duplicated between the daily job and per-date runs
- Add run_parity_check_for_date(date) which runs the full comparison +
alert upsert for a single date
Scraper integration:
- _safe_parity_recheck(date) wrapper (never raises) called after each
successful date in both search-results and hotel-page workers; hotel-page
mode waits until all hotels for the date are done before rechecking
API:
- POST /competitors/parity/check-date?rate_date=YYYY-MM-DD for manual recheck
Frontend:
- Recheck button on every parity alert row (all statuses); invalidates
alerts list and badge count on success
- parity-alert-count badge polls every 15s (was 60s) so new alerts from
the scheduled job or post-scrape rechecks appear quickly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DISTINCT ON tiebreaker was undefined when scraper writes multiple room
types in one batch (same scraped_at); adding rate_gross ASC ensures we
always pick the cheapest (lead-in / best available) rate, matching like
for like against the Newbook BAR tariff
- Past-date active alerts were never touched (start = today meant they
fell outside the query window); now resolved at the top of each run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parity_match_mode config ('best_available' default | 'match_terms').
Best available = cheapest bookable non-dinner direct tariff vs the BC
lead-in (already BC's best available) — simplest like-for-like. Term
matching kept as an option for days where BC's cheapest basis differs
from direct's. Settings gains a comparison-basis select.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The check compared Newbook's FIRST tariff (typically flexi B&B) against
Booking.com's lead-in card (often prepaid) — apples vs oranges, e.g.
21 Jul flagged B&B FLEX £289 vs a prepaid BC rate whose true comparable
was B&B PPAY £279.
- classify Newbook tariffs from names/descriptions (PPAY/prepay/advance/
ADV/NRF/saver = prepaid; DBB/dinner/half board = dinner-inclusive)
- use the scraped BC rate's flags (free_cancellation/no_prepayment/
breakfast_included) to pick the cheapest COMPARABLE tariff per date,
excluding tariffs not bookable for that date (success=false, min-stay
>1, unmet advance-purchase windows)
- tiered fallback (matched -> any non-dinner -> any -> legacy headline),
recorded per alert in room_category as 'TARIFF vs BC basis'
- shared gather_comparisons() now drives both the daily job and
GET /competitors/parity (issues gain newbook_tariff, booking_basis,
match_quality, expected_rate)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- config: parity_markup_value/_unit + parity_tolerance_value/_unit
(pct|gbp), legacy *_pct keys still read as fallback
- expected rate = newbook + £X or newbook × (1 + X%); breach test uses
the tolerance in its own unit
- Settings parity tab: unit selects + live worked example line
- Parity Alerts tab: unit-aware description, badge now shows £ deviation
alongside %
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parity was read-only — the alerts table had no producer, so the Market
View badge could never fire. Now:
- jobs/check_rate_parity.py: daily 06:45 job comparing own Booking.com
lead-in rate vs cheapest Newbook rate per date, measured against an
EXPECTED markup (we deliberately price Booking.com higher to cover
commission): alert when deviation from newbook*(1+markup%) exceeds the
tolerance. Creates/updates active alerts, auto-resolves dates back in
line, leaves acknowledged dates alone.
- config keys: parity_check_enabled, parity_expected_markup_pct,
parity_tolerance_pct (system_config)
- POST /competitors/parity/check manual trigger; GET /parity now uses the
same markup/tolerance and cheapest-across-categories Newbook rate
- Settings -> Rate Parity tab: markup %, tolerance %, enable toggle,
run-now with result summary
- Market View -> Parity Alerts tab: status-filtered list w/ acknowledge
- Market View -> Hotels: direct-link dropdown per competitor (new PUT
/competitors/hotels/{id}/direct-link) — closes the never-written
direct_hotel_id gap so the matrix direct-rates sub-row can populate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>