Parity: like-for-like tariff matching instead of headline vs lead-in
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>
This commit is contained in:
parent
9545fd651d
commit
3b501f95c5
2 changed files with 204 additions and 133 deletions
|
|
@ -717,93 +717,45 @@ async def get_rate_parity(
|
|||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get rate parity comparison between booking.com and Newbook rates.
|
||||
|
||||
Compares scraped booking.com rates for own hotel against Newbook current rates.
|
||||
Like-for-like parity comparison between our Booking.com rate and the
|
||||
comparable Newbook tariff (matched on flex/prepaid + board via the
|
||||
scraped rate's condition flags). Same logic as the daily alert job.
|
||||
"""
|
||||
import asyncio
|
||||
from jobs.check_rate_parity import get_parity_config, gather_comparisons, expected_booking_rate
|
||||
from database import SyncSessionLocal
|
||||
|
||||
today = date.today()
|
||||
start = date.fromisoformat(from_date) if from_date else today
|
||||
end = date.fromisoformat(to_date) if to_date else today + timedelta(days=30)
|
||||
|
||||
# Get own hotel's booking.com rates
|
||||
booking_rates_result = await db.execute(
|
||||
text("""
|
||||
SELECT DISTINCT ON (r.rate_date)
|
||||
r.rate_date,
|
||||
r.rate_gross as booking_rate,
|
||||
r.availability_status,
|
||||
r.room_type as booking_room_type,
|
||||
r.scraped_at
|
||||
FROM booking_com_rates r
|
||||
JOIN booking_com_hotels h ON r.hotel_id = h.id
|
||||
WHERE h.tier = 'own'
|
||||
AND r.rate_date >= :from_date AND r.rate_date <= :to_date
|
||||
ORDER BY r.rate_date, r.scraped_at DESC
|
||||
"""),
|
||||
{'from_date': start, 'to_date': end}
|
||||
)
|
||||
booking_rates = {row.rate_date: dict(row._mapping) for row in booking_rates_result.fetchall()}
|
||||
def _run():
|
||||
sdb = SyncSessionLocal()
|
||||
try:
|
||||
cfg = get_parity_config(sdb)
|
||||
return cfg, gather_comparisons(sdb, start, end, cfg)
|
||||
finally:
|
||||
sdb.close()
|
||||
|
||||
# Get Newbook rates (cheapest per date across latest category snapshots)
|
||||
newbook_rates_result = await db.execute(
|
||||
text("""
|
||||
SELECT rate_date, MIN(rate_gross) AS newbook_rate FROM (
|
||||
SELECT DISTINCT ON (rate_date, category_id)
|
||||
rate_date, rate_gross
|
||||
FROM newbook_current_rates
|
||||
WHERE rate_date >= :from_date AND rate_date <= :to_date
|
||||
AND rate_gross IS NOT NULL AND rate_gross > 0
|
||||
ORDER BY rate_date, category_id, valid_from DESC
|
||||
) t GROUP BY rate_date
|
||||
"""),
|
||||
{'from_date': start, 'to_date': end}
|
||||
)
|
||||
newbook_rates = {row.rate_date: dict(row._mapping) for row in newbook_rates_result.fetchall()}
|
||||
loop = asyncio.get_event_loop()
|
||||
cfg, comparisons = await loop.run_in_executor(None, _run)
|
||||
|
||||
# Expected-markup config: we deliberately price Booking.com higher to
|
||||
# cover commission, so parity is measured against Newbook + markup (% or £)
|
||||
from jobs.check_rate_parity import get_parity_config, expected_booking_rate, evaluate_parity
|
||||
from database import SyncSessionLocal
|
||||
cfg_db = SyncSessionLocal()
|
||||
try:
|
||||
cfg = get_parity_config(cfg_db)
|
||||
finally:
|
||||
cfg_db.close()
|
||||
|
||||
# Compare rates
|
||||
parity_issues = []
|
||||
all_dates = set(booking_rates.keys()) | set(newbook_rates.keys())
|
||||
|
||||
for rate_date in sorted(all_dates):
|
||||
booking = booking_rates.get(rate_date)
|
||||
newbook = newbook_rates.get(rate_date)
|
||||
|
||||
if not booking or not newbook:
|
||||
continue
|
||||
|
||||
booking_rate = booking.get('booking_rate')
|
||||
newbook_rate = newbook.get('newbook_rate')
|
||||
|
||||
if not booking_rate or not newbook_rate:
|
||||
continue
|
||||
|
||||
result = evaluate_parity(float(booking_rate), float(newbook_rate), cfg)
|
||||
if result is None:
|
||||
continue
|
||||
diff_pct, diff_gbp, breach = result
|
||||
|
||||
if breach:
|
||||
parity_issues.append({
|
||||
'rate_date': rate_date.isoformat(),
|
||||
'booking_rate': float(booking_rate),
|
||||
'newbook_rate': float(newbook_rate),
|
||||
'expected_rate': round(expected_booking_rate(float(newbook_rate), cfg), 2),
|
||||
'difference_pct': round(diff_pct, 2),
|
||||
'difference_gbp': round(diff_gbp, 2),
|
||||
'alert_type': 'higher' if diff_pct > 0 else 'lower',
|
||||
'booking_room_type': booking.get('booking_room_type'),
|
||||
'availability_status': booking.get('availability_status'),
|
||||
})
|
||||
parity_issues = [
|
||||
{
|
||||
'rate_date': c["date"].isoformat(),
|
||||
'booking_rate': c["booking_rate"],
|
||||
'booking_basis': c["booking_basis"],
|
||||
'booking_room_type': c["booking_room"],
|
||||
'newbook_rate': c["newbook_rate"],
|
||||
'newbook_tariff': c["newbook_tariff"],
|
||||
'match_quality': c["match_quality"],
|
||||
'expected_rate': round(expected_booking_rate(c["newbook_rate"], cfg), 2),
|
||||
'difference_pct': round(c["dev_pct"], 2),
|
||||
'difference_gbp': round(c["dev_gbp"], 2),
|
||||
'alert_type': 'higher' if c["dev_pct"] > 0 else 'lower',
|
||||
}
|
||||
for c in comparisons if c["breach"]
|
||||
]
|
||||
|
||||
return {
|
||||
'from_date': start.isoformat(),
|
||||
|
|
@ -812,6 +764,7 @@ async def get_rate_parity(
|
|||
'markup_unit': cfg["markup_unit"],
|
||||
'tolerance_value': cfg["tolerance_value"],
|
||||
'tolerance_unit': cfg["tolerance_unit"],
|
||||
'dates_compared': len(comparisons),
|
||||
'issues_count': len(parity_issues),
|
||||
'issues': parity_issues
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue