Auto-recheck parity after scrape + manual Recheck button + faster badge
Parity logic: - Extract _fetch_latest_alerts() and _upsert_alerts() helpers so the alert upsert loop is no longer duplicated between the daily job and per-date runs - Add run_parity_check_for_date(date) which runs the full comparison + alert upsert for a single date Scraper integration: - _safe_parity_recheck(date) wrapper (never raises) called after each successful date in both search-results and hotel-page workers; hotel-page mode waits until all hotels for the date are done before rechecking API: - POST /competitors/parity/check-date?rate_date=YYYY-MM-DD for manual recheck Frontend: - Recheck button on every parity alert row (all statuses); invalidates alerts list and badge count on success - parity-alert-count badge polls every 15s (was 60s) so new alerts from the scheduled job or post-scrape rechecks appear quickly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8a80c66a3b
commit
cab1a39503
5 changed files with 146 additions and 66 deletions
|
|
@ -244,6 +244,93 @@ def gather_comparisons(db, start: date, end: date, cfg: dict) -> list:
|
|||
return out
|
||||
|
||||
|
||||
def _fetch_latest_alerts(db, start: date, end: date) -> dict:
|
||||
"""Return {rate_date: {id, status, diff}} for the most-recent alert per date in [start, end]."""
|
||||
rows = db.execute(text("""
|
||||
SELECT DISTINCT ON (rate_date)
|
||||
id, rate_date, alert_status, difference_pct
|
||||
FROM rate_parity_alerts
|
||||
WHERE rate_date BETWEEN :fd AND :td
|
||||
ORDER BY rate_date, created_at DESC
|
||||
"""), {"fd": start, "td": end}).fetchall()
|
||||
return {r[1]: {"id": r[0], "status": r[2], "diff": float(r[3] or 0)} for r in rows}
|
||||
|
||||
|
||||
def _upsert_alerts(db, comparisons: list, latest_alert: dict) -> tuple:
|
||||
"""Apply parity comparison results to the alerts table.
|
||||
Returns (created, updated, resolved)."""
|
||||
created = updated = resolved = 0
|
||||
for c in comparisons:
|
||||
alert = latest_alert.get(c["date"])
|
||||
category_label = f"{c['newbook_tariff']} vs BC {c['booking_basis']}"[:255]
|
||||
|
||||
if c["breach"]:
|
||||
if alert and alert["status"] == "active":
|
||||
if round(c["dev_pct"], 2) != round(alert["diff"], 2):
|
||||
db.execute(text("""
|
||||
UPDATE rate_parity_alerts
|
||||
SET newbook_rate = :nb, booking_com_rate = :bc,
|
||||
difference_pct = :diff, alert_type = :atype,
|
||||
room_category = :room
|
||||
WHERE id = :id
|
||||
"""), {
|
||||
"id": alert["id"], "nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||
"diff": round(c["dev_pct"], 2),
|
||||
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||
"room": category_label,
|
||||
})
|
||||
updated += 1
|
||||
elif alert and alert["status"] == "acknowledged":
|
||||
pass # user has dealt with this date — don't nag
|
||||
else:
|
||||
db.execute(text("""
|
||||
INSERT INTO rate_parity_alerts
|
||||
(rate_date, room_category, newbook_rate, booking_com_rate,
|
||||
difference_pct, alert_type, alert_status)
|
||||
VALUES (:date, :room, :nb, :bc, :diff, :atype, 'active')
|
||||
"""), {
|
||||
"date": c["date"], "room": category_label,
|
||||
"nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||
"diff": round(c["dev_pct"], 2),
|
||||
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||
})
|
||||
created += 1
|
||||
else:
|
||||
if alert and alert["status"] in ("active", "acknowledged"):
|
||||
db.execute(
|
||||
text("UPDATE rate_parity_alerts SET alert_status = 'resolved' WHERE id = :id"),
|
||||
{"id": alert["id"]}
|
||||
)
|
||||
resolved += 1
|
||||
|
||||
return created, updated, resolved
|
||||
|
||||
|
||||
def run_parity_check_for_date(rate_date: date) -> dict:
|
||||
"""Run the parity check for a single date.
|
||||
Called after a date is re-scraped, or triggered manually from the UI."""
|
||||
db = SyncSessionLocal()
|
||||
try:
|
||||
cfg = get_parity_config(db)
|
||||
if not cfg["enabled"]:
|
||||
return {"status": "disabled"}
|
||||
|
||||
comparisons = gather_comparisons(db, rate_date, rate_date, cfg)
|
||||
if not comparisons:
|
||||
return {"status": "ok", "date": str(rate_date), "no_data": True}
|
||||
|
||||
latest_alert = _fetch_latest_alerts(db, rate_date, rate_date)
|
||||
created, updated, resolved = _upsert_alerts(db, comparisons, latest_alert)
|
||||
db.commit()
|
||||
return {"status": "ok", "date": str(rate_date),
|
||||
"created": created, "updated": updated, "resolved": resolved}
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def run_parity_check() -> dict:
|
||||
db = SyncSessionLocal()
|
||||
try:
|
||||
|
|
@ -265,61 +352,8 @@ def run_parity_check() -> dict:
|
|||
"""), {"today": today})
|
||||
|
||||
comparisons = gather_comparisons(db, start, end, cfg)
|
||||
|
||||
# Latest alert per date in the horizon
|
||||
alert_rows = db.execute(text("""
|
||||
SELECT DISTINCT ON (rate_date)
|
||||
id, rate_date, alert_status, difference_pct
|
||||
FROM rate_parity_alerts
|
||||
WHERE rate_date BETWEEN :fd AND :td
|
||||
ORDER BY rate_date, created_at DESC
|
||||
"""), {"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}
|
||||
|
||||
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]
|
||||
|
||||
if c["breach"]:
|
||||
if alert and alert["status"] == "active":
|
||||
if round(c["dev_pct"], 2) != round(alert["diff"], 2):
|
||||
db.execute(text("""
|
||||
UPDATE rate_parity_alerts
|
||||
SET newbook_rate = :nb, booking_com_rate = :bc,
|
||||
difference_pct = :diff, alert_type = :atype,
|
||||
room_category = :room
|
||||
WHERE id = :id
|
||||
"""), {
|
||||
"id": alert["id"], "nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||
"diff": round(c["dev_pct"], 2),
|
||||
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||
"room": category_label,
|
||||
})
|
||||
updated += 1
|
||||
elif alert and alert["status"] == "acknowledged":
|
||||
pass # user has dealt with this date — don't nag
|
||||
else:
|
||||
db.execute(text("""
|
||||
INSERT INTO rate_parity_alerts
|
||||
(rate_date, room_category, newbook_rate, booking_com_rate,
|
||||
difference_pct, alert_type, alert_status)
|
||||
VALUES (:date, :room, :nb, :bc, :diff, :atype, 'active')
|
||||
"""), {
|
||||
"date": c["date"], "room": category_label,
|
||||
"nb": c["newbook_rate"], "bc": c["booking_rate"],
|
||||
"diff": round(c["dev_pct"], 2),
|
||||
"atype": "higher" if c["dev_pct"] > 0 else "lower",
|
||||
})
|
||||
created += 1
|
||||
else:
|
||||
if alert and alert["status"] in ("active", "acknowledged"):
|
||||
db.execute(
|
||||
text("UPDATE rate_parity_alerts SET alert_status = 'resolved' WHERE id = :id"),
|
||||
{"id": alert["id"]}
|
||||
)
|
||||
resolved += 1
|
||||
latest_alert = _fetch_latest_alerts(db, start, end)
|
||||
created, updated, resolved = _upsert_alerts(db, comparisons, latest_alert)
|
||||
|
||||
db.commit()
|
||||
summary = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue