From ad0e82465c1029741acfb53e571c916b69bd21ef Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Mon, 6 Jul 2026 08:04:15 +0000 Subject: [PATCH] Parity: best-available comparison as default, term-matching optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/jobs/check_rate_parity.py | 33 ++++++++++++++++++++----------- frontend/src/pages/Settings.tsx | 20 +++++++++++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/backend/jobs/check_rate_parity.py b/backend/jobs/check_rate_parity.py index 43c17cb..6b93dfb 100644 --- a/backend/jobs/check_rate_parity.py +++ b/backend/jobs/check_rate_parity.py @@ -24,6 +24,10 @@ Dates back within tolerance auto-resolve; acknowledged dates stay quiet. Config (system_config): parity_check_enabled true/false (default true) + parity_match_mode 'best_available' (default: cheapest bookable + non-dinner tariff vs the BC lead-in, which is + already BC's best available) or 'match_terms' + (flex vs flex, prepaid vs prepaid) parity_markup_value expected Booking.com premium over Newbook (default 0) parity_markup_unit 'pct' or 'gbp' (default pct) parity_tolerance_value allowed deviation from expected before alerting (default 2) @@ -74,6 +78,7 @@ def get_parity_config(db) -> dict: enabled_raw = (cfg.get('parity_check_enabled') or 'true').lower() return { "enabled": enabled_raw in ('true', '1', 'yes', 'enabled'), + "match_mode": 'match_terms' if (cfg.get('parity_match_mode') or '').lower() == 'match_terms' else 'best_available', "markup_value": num('parity_markup_value', 0.0, 'parity_expected_markup_pct'), "markup_unit": unit('parity_markup_unit'), "tolerance_value": num('parity_tolerance_value', 2.0, 'parity_tolerance_pct'), @@ -133,19 +138,24 @@ def _candidate_tariffs(tariffs_data, days_ahead: int) -> list: return out -def _pick_comparable(candidates: list, booking_flex: bool, booking_breakfast: bool): - """Cheapest tariff matching the Booking.com rate's basis, tiered fallback. - All our tariffs include breakfast, so board matching reduces to excluding - dinner-inclusive tariffs unless nothing else exists. +def _pick_comparable(candidates: list, booking_flex: bool, booking_breakfast: bool, + match_mode: str = 'best_available'): + """Newbook tariff to compare against the Booking.com lead-in. + best_available: cheapest bookable non-dinner tariff (the BC lead-in is + already BC's best available, so this is BAR vs BAR). + match_terms: cheapest tariff matching the BC rate's flex/prepaid basis, + falling back to best available. All our tariffs include breakfast, so + board matching reduces to excluding dinner-inclusive tariffs unless + nothing else exists. Returns (tariff, match_quality) or (None, None).""" if not candidates: return None, None - want_prepaid = not booking_flex - tiers = [ - ([c for c in candidates if c["prepaid"] == want_prepaid and not c["dinner"]], 'matched'), - ([c for c in candidates if not c["dinner"]], 'any'), - (candidates, 'any'), - ] + tiers = [] + if match_mode == 'match_terms': + want_prepaid = not booking_flex + tiers.append(([c for c in candidates if c["prepaid"] == want_prepaid and not c["dinner"]], 'matched')) + tiers.append(([c for c in candidates if not c["dinner"]], 'best available')) + tiers.append((candidates, 'best available')) for pool, quality in tiers: if pool: return min(pool, key=lambda c: c["rate"]), quality @@ -197,7 +207,7 @@ def gather_comparisons(db, start: date, end: date, cfg: dict) -> list: for cat in cat_rows: candidates.extend(_candidate_tariffs(cat["tariffs_data"], days_ahead)) - chosen, quality = _pick_comparable(candidates, booking_flex, booking_breakfast) + chosen, quality = _pick_comparable(candidates, booking_flex, booking_breakfast, cfg["match_mode"]) if chosen: newbook_rate = chosen["rate"] tariff_name = chosen["name"] @@ -305,6 +315,7 @@ def run_parity_check() -> dict: db.commit() summary = { "status": "ok", + "match_mode": cfg["match_mode"], "dates_compared": len(comparisons), "created": created, "updated": updated, diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index eee0a52..62b59db 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -167,6 +167,26 @@ function ParityTab({ config, isLoading, onSave, saving }: { Run daily parity check +
+ + +
+ Best available compares our cheapest bookable direct tariff against the Booking.com + lead-in rate (Booking.com's best available). Match terms instead picks the direct tariff + with the same conditions as the scraped Booking.com rate — only differs on days where + Booking.com's cheapest is flexible while a cheaper prepaid exists direct. +
+
+