Port occupancy sync + category ordering from the forecasting version
- Bookability showed no availability because newbook_occupancy_report_data was never populated: add reports_occupancy client method and sync_occupancy job, run before rates in both Sync Now and the daily schedule (single fast API call) - Category order: default display_order to the Newbook category id on sync (was 0 → alphabetical), preserve manual order on re-sync, extend PATCH to accept display_order, add up/down reorder arrows in Settings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c924d793e8
commit
f51e1dcb76
5 changed files with 199 additions and 9 deletions
94
backend/jobs/sync_occupancy.py
Normal file
94
backend/jobs/sync_occupancy.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
Occupancy report sync — populates newbook_occupancy_report_data so the
|
||||
Bookability matrix can show availability alongside rates.
|
||||
"""
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def run_sync_occupancy(days_ahead: int = 365):
|
||||
"""
|
||||
Fetch the Newbook occupancy report from today forward and upsert
|
||||
per-category, per-date availability into newbook_occupancy_report_data.
|
||||
"""
|
||||
from database import AsyncSessionLocal
|
||||
from services.newbook_rates_client import NewbookRatesClient
|
||||
|
||||
from_date = date.today()
|
||||
to_date = from_date + timedelta(days=days_ahead)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
client = await NewbookRatesClient.from_db(db)
|
||||
async with client:
|
||||
report = await client.get_occupancy_report(from_date, to_date)
|
||||
|
||||
logger.info(f"Occupancy report: {len(report)} categories, {from_date} to {to_date}")
|
||||
vat = float(client.vat_rate or 0)
|
||||
records = 0
|
||||
|
||||
for category in report:
|
||||
category_id = str(category.get("category_id") or "")
|
||||
category_name = category.get("category_name") or ""
|
||||
if not category_id:
|
||||
continue
|
||||
|
||||
for date_str, day in (category.get("occupancy") or {}).items():
|
||||
try:
|
||||
report_date = date.fromisoformat(date_str) if isinstance(date_str, str) else date_str
|
||||
available = int(day.get("available", 0) or 0)
|
||||
occupied = int(day.get("occupied", 0) or 0)
|
||||
maintenance = int(day.get("maintenance", 0) or 0)
|
||||
allotted = int(day.get("allotted", 0) or 0)
|
||||
revenue_gross = float(day.get("revenue_gross", 0) or 0)
|
||||
revenue_net = day.get("revenue_net")
|
||||
if revenue_net is None:
|
||||
revenue_net = revenue_gross / (1 + vat) if vat else revenue_gross
|
||||
occupancy_pct = (occupied / available * 100) if available > 0 else 0
|
||||
|
||||
await db.execute(
|
||||
text("""
|
||||
INSERT INTO newbook_occupancy_report_data (
|
||||
date, category_id, category_name,
|
||||
available, occupied, maintenance, allotted,
|
||||
revenue_gross, revenue_net, occupancy_pct, fetched_at
|
||||
) VALUES (
|
||||
:date, :category_id, :category_name,
|
||||
:available, :occupied, :maintenance, :allotted,
|
||||
:revenue_gross, :revenue_net, :occupancy_pct, NOW()
|
||||
)
|
||||
ON CONFLICT (date, category_id) DO UPDATE SET
|
||||
category_name = EXCLUDED.category_name,
|
||||
available = EXCLUDED.available,
|
||||
occupied = EXCLUDED.occupied,
|
||||
maintenance = EXCLUDED.maintenance,
|
||||
allotted = EXCLUDED.allotted,
|
||||
revenue_gross = EXCLUDED.revenue_gross,
|
||||
revenue_net = EXCLUDED.revenue_net,
|
||||
occupancy_pct = EXCLUDED.occupancy_pct,
|
||||
fetched_at = NOW()
|
||||
"""),
|
||||
{
|
||||
"date": report_date,
|
||||
"category_id": category_id,
|
||||
"category_name": category_name,
|
||||
"available": available,
|
||||
"occupied": occupied,
|
||||
"maintenance": maintenance,
|
||||
"allotted": allotted,
|
||||
"revenue_gross": round(revenue_gross, 2),
|
||||
"revenue_net": round(revenue_net, 2),
|
||||
"occupancy_pct": round(occupancy_pct, 2),
|
||||
}
|
||||
)
|
||||
records += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Skipping occupancy row {category_id}/{date_str}: {e}")
|
||||
await db.rollback()
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Occupancy sync complete: {records} records upserted")
|
||||
return records
|
||||
Loading…
Add table
Add a link
Reference in a new issue