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)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get rate parity comparison between booking.com and Newbook rates.
|
Like-for-like parity comparison between our Booking.com rate and the
|
||||||
|
comparable Newbook tariff (matched on flex/prepaid + board via the
|
||||||
Compares scraped booking.com rates for own hotel against Newbook current rates.
|
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()
|
today = date.today()
|
||||||
start = date.fromisoformat(from_date) if from_date else today
|
start = date.fromisoformat(from_date) if from_date else today
|
||||||
end = date.fromisoformat(to_date) if to_date else today + timedelta(days=30)
|
end = date.fromisoformat(to_date) if to_date else today + timedelta(days=30)
|
||||||
|
|
||||||
# Get own hotel's booking.com rates
|
def _run():
|
||||||
booking_rates_result = await db.execute(
|
sdb = SyncSessionLocal()
|
||||||
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()}
|
|
||||||
|
|
||||||
# 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()}
|
|
||||||
|
|
||||||
# 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:
|
try:
|
||||||
cfg = get_parity_config(cfg_db)
|
cfg = get_parity_config(sdb)
|
||||||
|
return cfg, gather_comparisons(sdb, start, end, cfg)
|
||||||
finally:
|
finally:
|
||||||
cfg_db.close()
|
sdb.close()
|
||||||
|
|
||||||
# Compare rates
|
loop = asyncio.get_event_loop()
|
||||||
parity_issues = []
|
cfg, comparisons = await loop.run_in_executor(None, _run)
|
||||||
all_dates = set(booking_rates.keys()) | set(newbook_rates.keys())
|
|
||||||
|
|
||||||
for rate_date in sorted(all_dates):
|
parity_issues = [
|
||||||
booking = booking_rates.get(rate_date)
|
{
|
||||||
newbook = newbook_rates.get(rate_date)
|
'rate_date': c["date"].isoformat(),
|
||||||
|
'booking_rate': c["booking_rate"],
|
||||||
if not booking or not newbook:
|
'booking_basis': c["booking_basis"],
|
||||||
continue
|
'booking_room_type': c["booking_room"],
|
||||||
|
'newbook_rate': c["newbook_rate"],
|
||||||
booking_rate = booking.get('booking_rate')
|
'newbook_tariff': c["newbook_tariff"],
|
||||||
newbook_rate = newbook.get('newbook_rate')
|
'match_quality': c["match_quality"],
|
||||||
|
'expected_rate': round(expected_booking_rate(c["newbook_rate"], cfg), 2),
|
||||||
if not booking_rate or not newbook_rate:
|
'difference_pct': round(c["dev_pct"], 2),
|
||||||
continue
|
'difference_gbp': round(c["dev_gbp"], 2),
|
||||||
|
'alert_type': 'higher' if c["dev_pct"] > 0 else 'lower',
|
||||||
result = evaluate_parity(float(booking_rate), float(newbook_rate), cfg)
|
}
|
||||||
if result is None:
|
for c in comparisons if c["breach"]
|
||||||
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'),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'from_date': start.isoformat(),
|
'from_date': start.isoformat(),
|
||||||
|
|
@ -812,6 +764,7 @@ async def get_rate_parity(
|
||||||
'markup_unit': cfg["markup_unit"],
|
'markup_unit': cfg["markup_unit"],
|
||||||
'tolerance_value': cfg["tolerance_value"],
|
'tolerance_value': cfg["tolerance_value"],
|
||||||
'tolerance_unit': cfg["tolerance_unit"],
|
'tolerance_unit': cfg["tolerance_unit"],
|
||||||
|
'dates_compared': len(comparisons),
|
||||||
'issues_count': len(parity_issues),
|
'issues_count': len(parity_issues),
|
||||||
'issues': parity_issues
|
'issues': parity_issues
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,26 @@
|
||||||
"""
|
"""
|
||||||
Rate Parity Check Job
|
Rate Parity Check Job
|
||||||
|
|
||||||
Compares our own hotel's scraped Booking.com rate against our Newbook rate
|
Compares our own hotel's scraped Booking.com rate against the COMPARABLE
|
||||||
for each date, allowing for a configured expected markup — we deliberately
|
Newbook tariff for each date — like-for-like, not headline vs headline.
|
||||||
price Booking.com higher to cover commission, so parity is measured against
|
|
||||||
expected = newbook × (1 + markup%), not raw equality. Deviations beyond the
|
The scraped Booking.com lead-in rate carries condition flags (breakfast
|
||||||
tolerance are persisted as rate_parity_alerts rows (feeding the Market View
|
included, free cancellation, no prepayment); Newbook's tariffs_data holds
|
||||||
badge); dates back within tolerance auto-resolve their active alert.
|
every tariff by name with per-day rates (e.g. DIRECT B&B FLEX / PPAY / ADV,
|
||||||
Acknowledged alerts are left alone — acknowledging a date suppresses
|
DIRECT DBB ...). We classify tariffs from their names and pick the cheapest
|
||||||
re-alerting for it until the alert is resolved by the rates coming back
|
one matching the Booking.com rate's basis: a prepaid B&B rate on Booking.com
|
||||||
in line.
|
is compared against the cheapest prepaid B&B tariff, not the flexi rate.
|
||||||
|
|
||||||
|
Fallback tiers when no exact match exists (each alert records which was used):
|
||||||
|
matched — terms (flex/prepaid) and board match
|
||||||
|
terms — terms match, any board except dinner-inclusive
|
||||||
|
any — any non-dinner tariff
|
||||||
|
headline — legacy rate_gross (first tariff) when tariffs_data is absent
|
||||||
|
|
||||||
|
Parity is then measured against an expected markup — we deliberately price
|
||||||
|
Booking.com higher to cover commission: expected = comparable + markup
|
||||||
|
(% or flat £), alert when the deviation exceeds the tolerance (% or £).
|
||||||
|
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)
|
||||||
|
|
@ -21,7 +32,9 @@ Config (system_config):
|
||||||
|
|
||||||
Schedule: daily at 06:45, after the 05:20 Newbook fetch and 05:30 scrape.
|
Schedule: daily at 06:45, after the 05:20 Newbook fetch and 05:30 scrape.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
@ -32,6 +45,9 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
HORIZON_DAYS = 90
|
HORIZON_DAYS = 90
|
||||||
|
|
||||||
|
PREPAID_PATTERNS = ('ppay', 'prepay', 'pre-pay', 'advance', 'adv', 'non-ref', 'nonref', 'no refund', 'nrf', 'saver')
|
||||||
|
DINNER_PATTERNS = ('dbb', 'dinner', 'half board', 'half-board')
|
||||||
|
|
||||||
|
|
||||||
def get_parity_config(db) -> dict:
|
def get_parity_config(db) -> dict:
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
|
|
@ -88,6 +104,136 @@ def evaluate_parity(booking_rate: float, newbook_rate: float, cfg: dict):
|
||||||
return dev_pct, dev_gbp, breach
|
return dev_pct, dev_gbp, breach
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_tariff(name: str, description: str = '') -> dict:
|
||||||
|
s = f"{name} {description}".lower()
|
||||||
|
is_dinner = any(p in s for p in DINNER_PATTERNS)
|
||||||
|
is_prepaid = any(re.search(rf'\b{re.escape(p)}\b', s) if p == 'adv' else (p in s)
|
||||||
|
for p in PREPAID_PATTERNS)
|
||||||
|
return {"prepaid": is_prepaid, "dinner": is_dinner}
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_tariffs(tariffs_data, days_ahead: int) -> list:
|
||||||
|
"""Bookable-tonight tariff options from one category's tariffs_data."""
|
||||||
|
if isinstance(tariffs_data, str):
|
||||||
|
try:
|
||||||
|
tariffs_data = json.loads(tariffs_data)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for t in (tariffs_data or {}).get('tariffs', []):
|
||||||
|
rate = t.get('rate')
|
||||||
|
if not t.get('success') or not rate or rate <= 0:
|
||||||
|
continue
|
||||||
|
if (t.get('min_stay') or 1) > 1:
|
||||||
|
continue
|
||||||
|
if t.get('min_advance_days') and days_ahead < t['min_advance_days']:
|
||||||
|
continue
|
||||||
|
cls = _classify_tariff(t.get('name', ''), t.get('description', ''))
|
||||||
|
out.append({"rate": float(rate), "name": t.get('name', 'Unknown'), **cls})
|
||||||
|
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.
|
||||||
|
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'),
|
||||||
|
]
|
||||||
|
for pool, quality in tiers:
|
||||||
|
if pool:
|
||||||
|
return min(pool, key=lambda c: c["rate"]), quality
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def gather_comparisons(db, start: date, end: date, cfg: dict) -> list:
|
||||||
|
"""Per-date like-for-like comparison rows for the horizon.
|
||||||
|
Each row: {date, booking_rate, booking_room, booking_basis, newbook_rate,
|
||||||
|
newbook_tariff, match_quality, dev_pct, dev_gbp, breach}"""
|
||||||
|
booking_rows = db.execute(text("""
|
||||||
|
SELECT DISTINCT ON (r.rate_date)
|
||||||
|
r.rate_date, r.rate_gross, r.room_type,
|
||||||
|
r.breakfast_included, r.free_cancellation, r.no_prepayment
|
||||||
|
FROM booking_com_rates r
|
||||||
|
JOIN booking_com_hotels h ON r.hotel_id = h.id
|
||||||
|
WHERE h.tier = 'own'
|
||||||
|
AND r.rate_date BETWEEN :fd AND :td
|
||||||
|
AND r.rate_gross IS NOT NULL
|
||||||
|
ORDER BY r.rate_date, r.scraped_at DESC
|
||||||
|
"""), {"fd": start, "td": end}).mappings().all()
|
||||||
|
|
||||||
|
newbook_rows = db.execute(text("""
|
||||||
|
SELECT DISTINCT ON (rate_date, category_id)
|
||||||
|
rate_date, category_id, rate_gross, tariffs_data
|
||||||
|
FROM newbook_current_rates
|
||||||
|
WHERE rate_date BETWEEN :fd AND :td
|
||||||
|
ORDER BY rate_date, category_id, valid_from DESC
|
||||||
|
"""), {"fd": start, "td": end}).mappings().all()
|
||||||
|
|
||||||
|
newbook_by_date: dict = {}
|
||||||
|
for r in newbook_rows:
|
||||||
|
newbook_by_date.setdefault(r["rate_date"], []).append(r)
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
out = []
|
||||||
|
for b in booking_rows:
|
||||||
|
d = b["rate_date"]
|
||||||
|
cat_rows = newbook_by_date.get(d)
|
||||||
|
if not cat_rows:
|
||||||
|
continue
|
||||||
|
|
||||||
|
booking_rate = float(b["rate_gross"])
|
||||||
|
booking_flex = bool(b["free_cancellation"] or b["no_prepayment"])
|
||||||
|
booking_breakfast = bool(b["breakfast_included"])
|
||||||
|
days_ahead = (d - today).days
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for cat in cat_rows:
|
||||||
|
candidates.extend(_candidate_tariffs(cat["tariffs_data"], days_ahead))
|
||||||
|
|
||||||
|
chosen, quality = _pick_comparable(candidates, booking_flex, booking_breakfast)
|
||||||
|
if chosen:
|
||||||
|
newbook_rate = chosen["rate"]
|
||||||
|
tariff_name = chosen["name"]
|
||||||
|
else:
|
||||||
|
# No tariff detail — legacy fallback to cheapest headline rate
|
||||||
|
headline = [float(c["rate_gross"]) for c in cat_rows if c["rate_gross"] and float(c["rate_gross"]) > 0]
|
||||||
|
if not headline:
|
||||||
|
continue
|
||||||
|
newbook_rate = min(headline)
|
||||||
|
tariff_name = 'headline rate'
|
||||||
|
quality = 'headline'
|
||||||
|
|
||||||
|
result = evaluate_parity(booking_rate, newbook_rate, cfg)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
dev_pct, dev_gbp, breach = result
|
||||||
|
|
||||||
|
basis_bits = [
|
||||||
|
'flex' if booking_flex else 'prepaid',
|
||||||
|
'B&B' if booking_breakfast else 'room only',
|
||||||
|
]
|
||||||
|
out.append({
|
||||||
|
"date": d,
|
||||||
|
"booking_rate": booking_rate,
|
||||||
|
"booking_room": (b["room_type"] or '').split('\n')[0].strip() or None,
|
||||||
|
"booking_basis": '/'.join(basis_bits),
|
||||||
|
"newbook_rate": newbook_rate,
|
||||||
|
"newbook_tariff": tariff_name,
|
||||||
|
"match_quality": quality,
|
||||||
|
"dev_pct": dev_pct,
|
||||||
|
"dev_gbp": dev_gbp,
|
||||||
|
"breach": breach,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def run_parity_check() -> dict:
|
def run_parity_check() -> dict:
|
||||||
db = SyncSessionLocal()
|
db = SyncSessionLocal()
|
||||||
try:
|
try:
|
||||||
|
|
@ -99,31 +245,7 @@ def run_parity_check() -> dict:
|
||||||
start = date.today()
|
start = date.today()
|
||||||
end = start + timedelta(days=HORIZON_DAYS)
|
end = start + timedelta(days=HORIZON_DAYS)
|
||||||
|
|
||||||
# Own hotel's latest Booking.com lead-in rate per date
|
comparisons = gather_comparisons(db, start, end, cfg)
|
||||||
booking_rows = db.execute(text("""
|
|
||||||
SELECT DISTINCT ON (r.rate_date)
|
|
||||||
r.rate_date, r.rate_gross, r.room_type
|
|
||||||
FROM booking_com_rates r
|
|
||||||
JOIN booking_com_hotels h ON r.hotel_id = h.id
|
|
||||||
WHERE h.tier = 'own'
|
|
||||||
AND r.rate_date BETWEEN :fd AND :td
|
|
||||||
AND r.rate_gross IS NOT NULL
|
|
||||||
ORDER BY r.rate_date, r.scraped_at DESC
|
|
||||||
"""), {"fd": start, "td": end}).fetchall()
|
|
||||||
booking = {r[0]: {"rate": float(r[1]), "room": r[2]} for r in booking_rows}
|
|
||||||
|
|
||||||
# Cheapest current Newbook rate per date (latest snapshot per category)
|
|
||||||
newbook_rows = 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 BETWEEN :fd AND :td
|
|
||||||
AND rate_gross IS NOT NULL AND rate_gross > 0
|
|
||||||
ORDER BY rate_date, category_id, valid_from DESC
|
|
||||||
) t GROUP BY rate_date
|
|
||||||
"""), {"fd": start, "td": end}).fetchall()
|
|
||||||
newbook = {r[0]: float(r[1]) for r in newbook_rows}
|
|
||||||
|
|
||||||
# Latest alert per date in the horizon
|
# Latest alert per date in the horizon
|
||||||
alert_rows = db.execute(text("""
|
alert_rows = db.execute(text("""
|
||||||
|
|
@ -135,19 +257,15 @@ def run_parity_check() -> dict:
|
||||||
"""), {"fd": start, "td": end}).fetchall()
|
"""), {"fd": start, "td": end}).fetchall()
|
||||||
latest_alert = {r[1]: {"id": r[0], "status": r[2], "diff": float(r[3] or 0)} for r in alert_rows}
|
latest_alert = {r[1]: {"id": r[0], "status": r[2], "diff": float(r[3] or 0)} for r in alert_rows}
|
||||||
|
|
||||||
common_dates = sorted(set(booking) & set(newbook))
|
|
||||||
created = updated = resolved = 0
|
created = updated = resolved = 0
|
||||||
|
for c in comparisons:
|
||||||
|
alert = latest_alert.get(c["date"])
|
||||||
|
# e.g. "DIRECT B&B PPAY vs BC prepaid/B&B"
|
||||||
|
category_label = f"{c['newbook_tariff']} vs BC {c['booking_basis']}"[:255]
|
||||||
|
|
||||||
for d in common_dates:
|
if c["breach"]:
|
||||||
result = evaluate_parity(booking[d]["rate"], newbook[d], cfg)
|
|
||||||
if result is None:
|
|
||||||
continue
|
|
||||||
dev, _dev_gbp, breach = result
|
|
||||||
alert = latest_alert.get(d)
|
|
||||||
|
|
||||||
if breach:
|
|
||||||
if alert and alert["status"] == "active":
|
if alert and alert["status"] == "active":
|
||||||
if round(dev, 2) != round(alert["diff"], 2):
|
if round(c["dev_pct"], 2) != round(alert["diff"], 2):
|
||||||
db.execute(text("""
|
db.execute(text("""
|
||||||
UPDATE rate_parity_alerts
|
UPDATE rate_parity_alerts
|
||||||
SET newbook_rate = :nb, booking_com_rate = :bc,
|
SET newbook_rate = :nb, booking_com_rate = :bc,
|
||||||
|
|
@ -155,10 +273,10 @@ def run_parity_check() -> dict:
|
||||||
room_category = :room
|
room_category = :room
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
"""), {
|
"""), {
|
||||||
"id": alert["id"], "nb": newbook[d], "bc": booking[d]["rate"],
|
"id": alert["id"], "nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||||
"diff": round(dev, 2),
|
"diff": round(c["dev_pct"], 2),
|
||||||
"atype": "higher" if dev > 0 else "lower",
|
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||||
"room": booking[d]["room"],
|
"room": category_label,
|
||||||
})
|
})
|
||||||
updated += 1
|
updated += 1
|
||||||
elif alert and alert["status"] == "acknowledged":
|
elif alert and alert["status"] == "acknowledged":
|
||||||
|
|
@ -170,10 +288,10 @@ def run_parity_check() -> dict:
|
||||||
difference_pct, alert_type, alert_status)
|
difference_pct, alert_type, alert_status)
|
||||||
VALUES (:date, :room, :nb, :bc, :diff, :atype, 'active')
|
VALUES (:date, :room, :nb, :bc, :diff, :atype, 'active')
|
||||||
"""), {
|
"""), {
|
||||||
"date": d, "room": booking[d]["room"],
|
"date": c["date"], "room": category_label,
|
||||||
"nb": newbook[d], "bc": booking[d]["rate"],
|
"nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||||
"diff": round(dev, 2),
|
"diff": round(c["dev_pct"], 2),
|
||||||
"atype": "higher" if dev > 0 else "lower",
|
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||||
})
|
})
|
||||||
created += 1
|
created += 1
|
||||||
else:
|
else:
|
||||||
|
|
@ -187,7 +305,7 @@ def run_parity_check() -> dict:
|
||||||
db.commit()
|
db.commit()
|
||||||
summary = {
|
summary = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"dates_compared": len(common_dates),
|
"dates_compared": len(comparisons),
|
||||||
"created": created,
|
"created": created,
|
||||||
"updated": updated,
|
"updated": updated,
|
||||||
"resolved": resolved,
|
"resolved": resolved,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue