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 +