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):
|
Config (system_config):
|
||||||
parity_check_enabled true/false (default true)
|
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_value expected Booking.com premium over Newbook (default 0)
|
||||||
parity_markup_unit 'pct' or 'gbp' (default pct)
|
parity_markup_unit 'pct' or 'gbp' (default pct)
|
||||||
parity_tolerance_value allowed deviation from expected before alerting (default 2)
|
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()
|
enabled_raw = (cfg.get('parity_check_enabled') or 'true').lower()
|
||||||
return {
|
return {
|
||||||
"enabled": enabled_raw in ('true', '1', 'yes', 'enabled'),
|
"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_value": num('parity_markup_value', 0.0, 'parity_expected_markup_pct'),
|
||||||
"markup_unit": unit('parity_markup_unit'),
|
"markup_unit": unit('parity_markup_unit'),
|
||||||
"tolerance_value": num('parity_tolerance_value', 2.0, 'parity_tolerance_pct'),
|
"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
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _pick_comparable(candidates: list, booking_flex: bool, booking_breakfast: bool):
|
def _pick_comparable(candidates: list, booking_flex: bool, booking_breakfast: bool,
|
||||||
"""Cheapest tariff matching the Booking.com rate's basis, tiered fallback.
|
match_mode: str = 'best_available'):
|
||||||
All our tariffs include breakfast, so board matching reduces to excluding
|
"""Newbook tariff to compare against the Booking.com lead-in.
|
||||||
dinner-inclusive tariffs unless nothing else exists.
|
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)."""
|
Returns (tariff, match_quality) or (None, None)."""
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None, None
|
return None, None
|
||||||
want_prepaid = not booking_flex
|
tiers = []
|
||||||
tiers = [
|
if match_mode == 'match_terms':
|
||||||
([c for c in candidates if c["prepaid"] == want_prepaid and not c["dinner"]], 'matched'),
|
want_prepaid = not booking_flex
|
||||||
([c for c in candidates if not c["dinner"]], 'any'),
|
tiers.append(([c for c in candidates if c["prepaid"] == want_prepaid and not c["dinner"]], 'matched'))
|
||||||
(candidates, 'any'),
|
tiers.append(([c for c in candidates if not c["dinner"]], 'best available'))
|
||||||
]
|
tiers.append((candidates, 'best available'))
|
||||||
for pool, quality in tiers:
|
for pool, quality in tiers:
|
||||||
if pool:
|
if pool:
|
||||||
return min(pool, key=lambda c: c["rate"]), quality
|
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:
|
for cat in cat_rows:
|
||||||
candidates.extend(_candidate_tariffs(cat["tariffs_data"], days_ahead))
|
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:
|
if chosen:
|
||||||
newbook_rate = chosen["rate"]
|
newbook_rate = chosen["rate"]
|
||||||
tariff_name = chosen["name"]
|
tariff_name = chosen["name"]
|
||||||
|
|
@ -305,6 +315,7 @@ def run_parity_check() -> dict:
|
||||||
db.commit()
|
db.commit()
|
||||||
summary = {
|
summary = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
|
"match_mode": cfg["match_mode"],
|
||||||
"dates_compared": len(comparisons),
|
"dates_compared": len(comparisons),
|
||||||
"created": created,
|
"created": created,
|
||||||
"updated": updated,
|
"updated": updated,
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,26 @@ function ParityTab({ config, isLoading, onSave, saving }: {
|
||||||
Run daily parity check
|
Run daily parity check
|
||||||
</label>
|
</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 style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-mid)', display: 'block', marginBottom: 4 }}>
|
<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