Show own hotel's direct rate (Newbook best available) in the matrix direct sub-row

New GET /competitors/own-direct-rates: cheapest bookable non-dinner
Newbook tariff per date (same selection rules as the parity check).
'Show direct rates' now renders a 'Direct (Newbook)' sub-row under the
own-hotel row alongside the competitors' scraped direct rows; cell
tooltip names the tariff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-06 08:25:46 +00:00
parent ad0e82465c
commit 2abc2b0297
2 changed files with 91 additions and 0 deletions

View file

@ -770,6 +770,66 @@ async def get_rate_parity(
}
@router.get("/own-direct-rates")
async def get_own_direct_rates(
from_date: Optional[str] = None,
to_date: Optional[str] = None,
current_user: dict = Depends(get_current_user)
):
"""Our own hotel's best-available direct rate per date, from Newbook —
cheapest bookable non-dinner tariff (same selection rules as the parity
check). Used for the own-hotel direct sub-row in the rate matrix."""
import asyncio
from jobs.check_rate_parity import _candidate_tariffs
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)
def _run():
sdb = SyncSessionLocal()
try:
rows = sdb.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()
finally:
sdb.close()
by_date: dict = {}
for r in rows:
by_date.setdefault(r["rate_date"], []).append(r)
out = {}
for d, cat_rows in by_date.items():
days_ahead = (d - today).days
candidates = []
for cat in cat_rows:
candidates.extend(_candidate_tariffs(cat["tariffs_data"], days_ahead))
pool = [c for c in candidates if not c["dinner"]] or candidates
if pool:
best = min(pool, key=lambda c: c["rate"])
out[d.isoformat()] = {"rate": best["rate"], "tariff": best["name"]}
else:
headline = [float(c["rate_gross"]) for c in cat_rows
if c["rate_gross"] and float(c["rate_gross"]) > 0]
if headline:
out[d.isoformat()] = {"rate": min(headline), "tariff": "headline rate"}
return out
loop = asyncio.get_event_loop()
rates = await loop.run_in_executor(None, _run)
return {
"from_date": start.isoformat(),
"to_date": end.isoformat(),
"rates": rates,
}
@router.post("/parity/check")
async def trigger_parity_check(
current_user: dict = Depends(get_current_user)