""" Occupancy report sync — populates newbook_occupancy_report_data so the Bookability matrix can show availability alongside rates. Uses snapshot model (mirrors newbook_current_rates): only inserts a new row when occupied/available/maintenance figures change; otherwise bumps last_verified_at on the existing row. This lets the history popup show how pick-up evolved over time for a given stay date. """ import logging from datetime import date, timedelta from sqlalchemy import text logger = logging.getLogger(__name__) async def _save_occupancy_snapshot(db, category_id, category_name, report_date, occupied, available, maintenance, allotted, revenue_gross, revenue_net, occupancy_pct): """ Insert a new occupancy snapshot only when figures have changed; otherwise update last_verified_at on the latest row. Returns 'inserted' or 'verified'. """ existing = (await db.execute( text(""" SELECT id, occupied, available, maintenance FROM newbook_occupancy_report_data WHERE date = :date AND category_id = :category_id ORDER BY valid_from DESC LIMIT 1 """), {"date": report_date, "category_id": category_id} )).fetchone() changed = ( existing is None or int(existing.occupied or 0) != occupied or int(existing.available or 0) != available or int(existing.maintenance or 0) != maintenance ) if changed: 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, valid_from, last_verified_at, fetched_at ) VALUES ( :date, :category_id, :category_name, :available, :occupied, :maintenance, :allotted, :revenue_gross, :revenue_net, :occupancy_pct, NOW(), NOW(), 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), } ) return 'inserted' else: await db.execute( text(""" UPDATE newbook_occupancy_report_data SET last_verified_at = NOW(), fetched_at = NOW() WHERE id = :id """), {"id": existing.id} ) return 'verified' async def run_sync_occupancy(days_ahead: int = 365): """ Fetch the Newbook occupancy report from today forward and snapshot 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) inserted = 0 verified = 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 result = await _save_occupancy_snapshot( db, category_id, category_name, report_date, occupied, available, maintenance, allotted, revenue_gross, revenue_net, occupancy_pct ) if result == 'inserted': inserted += 1 else: verified += 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: {inserted} new snapshots, {verified} unchanged") return inserted + verified