Rate parity: expected-markup config, alert writer job, alerts UI + direct-link UI

Parity was read-only — the alerts table had no producer, so the Market
View badge could never fire. Now:
- jobs/check_rate_parity.py: daily 06:45 job comparing own Booking.com
  lead-in rate vs cheapest Newbook rate per date, measured against an
  EXPECTED markup (we deliberately price Booking.com higher to cover
  commission): alert when deviation from newbook*(1+markup%) exceeds the
  tolerance. Creates/updates active alerts, auto-resolves dates back in
  line, leaves acknowledged dates alone.
- config keys: parity_check_enabled, parity_expected_markup_pct,
  parity_tolerance_pct (system_config)
- POST /competitors/parity/check manual trigger; GET /parity now uses the
  same markup/tolerance and cheapest-across-categories Newbook rate
- Settings -> Rate Parity tab: markup %, tolerance %, enable toggle,
  run-now with result summary
- Market View -> Parity Alerts tab: status-filtered list w/ acknowledge
- Market View -> Hotels: direct-link dropdown per competitor (new PUT
  /competitors/hotels/{id}/direct-link) — closes the never-written
  direct_hotel_id gap so the matrix direct-rates sub-row can populate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-06 07:30:41 +00:00
parent 029e3b379f
commit 493e87b2dc
5 changed files with 595 additions and 12 deletions

View file

@ -61,6 +61,13 @@ async def run_scheduled_direct_scrape():
await loop.run_in_executor(None, run_scrape_all_direct)
async def run_scheduled_parity_check():
from jobs.check_rate_parity import run_parity_check
import asyncio
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, run_parity_check)
async def run_scheduled_booking_scrape_async():
from jobs.scrape_booking_rates import run_scheduled_booking_scrape
import asyncio
@ -116,8 +123,16 @@ def start_scheduler():
replace_existing=True,
)
# Rate parity check — daily at 06:45, after Newbook fetch + Booking scrape
scheduler.add_job(
run_scheduled_parity_check,
CronTrigger(hour=6, minute=45),
id='parity_check',
replace_existing=True,
)
scheduler.start()
logger.info(f"Scheduler started: booking scrape at {scrape_hour:02d}:{scrape_minute:02d}, rates fetch at {rates_hour:02d}:{rates_minute:02d}, direct scrape at 06:00")
logger.info(f"Scheduler started: booking scrape at {scrape_hour:02d}:{scrape_minute:02d}, rates fetch at {rates_hour:02d}:{rates_minute:02d}, direct scrape at 06:00, parity check at 06:45")
def shutdown_scheduler():