Parity: best-available comparison as default, term-matching optional
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>
This commit is contained in:
parent
3b501f95c5
commit
ad0e82465c
2 changed files with 42 additions and 11 deletions
|
|
@ -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
|
||||
tiers = []
|
||||
if match_mode == 'match_terms':
|
||||
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.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,
|
||||
|
|
|
|||
|
|
@ -167,6 +167,26 @@ function ParityTab({ config, isLoading, onSave, saving }: {
|
|||
Run daily parity check
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-mid)', display: 'block', marginBottom: 4 }}>
|
||||
Comparison basis
|
||||
</label>
|
||||
<select
|
||||
style={{ width: 330 }}
|
||||
value={(config?.['parity_match_mode'] ?? 'best_available')}
|
||||
onChange={e => onSave('parity_match_mode', e.target.value)}
|
||||
>
|
||||
<option value="best_available">Best available (cheapest bookable tariff on both sides)</option>
|
||||
<option value="match_terms">Match terms (flex vs flex, prepaid vs prepaid)</option>
|
||||
</select>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4, maxWidth: 480 }}>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-mid)', display: 'block', marginBottom: 4 }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue