""" backfill_booking_statuses.py ──────────────────────────── One-time script to fix all existing bookings whose status should have automatically advanced but didn't because the transition job didn't exist yet. Rules applied ───────────── CONFIRMED → ACTIVE_STAY check-in passed but checkout has NOT yet passed CONFIRMED → COMPLETED both check-in AND checkout have passed ACTIVE_STAY → COMPLETED checkout has passed Run from the AIDA project root: python scripts/backfill_booking_statuses.py Set MONGO_URI environment variable (or edit the fallback below) before running. """ import asyncio import os from datetime import datetime from motor.motor_asyncio import AsyncIOMotorClient MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017") DB_NAME = os.getenv("MONGO_DB_NAME", "lojiz") STATUS_CONFIRMED = "Confirmed" STATUS_ACTIVE_STAY = "ActiveStay" STATUS_COMPLETED = "Completed" def _parse_dt(booking: dict, date_field: str, time_field: str, default_hour: int) -> datetime: raw = booking.get(date_field) if isinstance(raw, datetime): base = raw.replace(hour=0, minute=0, second=0, microsecond=0) elif isinstance(raw, str): try: base = datetime.fromisoformat(raw[:10]) except ValueError: return datetime.max else: return datetime.max time_str = booking.get(time_field) if time_str: try: h, m = (int(x) for x in time_str.strip().split(":")) return base.replace(hour=h, minute=m) except Exception: pass return base.replace(hour=default_hour, minute=0) async def run(): client = AsyncIOMotorClient(MONGO_URI) db = client[DB_NAME] now = datetime.utcnow() counts = {"confirmed_to_active": 0, "confirmed_to_completed": 0, "active_to_completed": 0, "skipped": 0} print(f"[{now.isoformat()}] Starting backfill — connecting to {DB_NAME}...") # ── Pass 1: Fix CONFIRMED bookings ─────────────────────────────────────── confirmed = await db["bookings"].find({"status": STATUS_CONFIRMED}).to_list(length=None) print(f" Found {len(confirmed)} CONFIRMED bookings to evaluate.") for b in confirmed: checkin_dt = _parse_dt(b, "check_in_date", "estimated_arrival_time", default_hour=14) checkout_dt = _parse_dt(b, "check_out_date", "estimated_departure_time", default_hour=11) if now < checkin_dt: counts["skipped"] += 1 continue if now >= checkout_dt: new_status = STATUS_COMPLETED counts["confirmed_to_completed"] += 1 else: new_status = STATUS_ACTIVE_STAY counts["confirmed_to_active"] += 1 await db["bookings"].update_one( {"_id": b["_id"]}, {"$set": {"status": new_status, "updated_at": now}}, ) print(f" Booking {b['_id']}: CONFIRMED → {new_status} " f"(check_in={checkin_dt.isoformat()}, check_out={checkout_dt.isoformat()})") # ── Pass 2: Fix ACTIVE_STAY bookings ───────────────────────────────────── active = await db["bookings"].find({"status": STATUS_ACTIVE_STAY}).to_list(length=None) print(f" Found {len(active)} ACTIVE_STAY bookings to evaluate.") for b in active: checkout_dt = _parse_dt(b, "check_out_date", "estimated_departure_time", default_hour=11) if now >= checkout_dt: await db["bookings"].update_one( {"_id": b["_id"]}, {"$set": {"status": STATUS_COMPLETED, "updated_at": now}}, ) counts["active_to_completed"] += 1 print(f" Booking {b['_id']}: ACTIVE_STAY → COMPLETED " f"(check_out={checkout_dt.isoformat()})") else: counts["skipped"] += 1 # ── Summary ─────────────────────────────────────────────────────────────── print("\n── Backfill complete ──────────────────────────────────────────") print(f" CONFIRMED → ACTIVE_STAY : {counts['confirmed_to_active']}") print(f" CONFIRMED → COMPLETED : {counts['confirmed_to_completed']}") print(f" ACTIVE_STAY → COMPLETED : {counts['active_to_completed']}") print(f" Skipped (not yet due) : {counts['skipped']}") total = counts["confirmed_to_active"] + counts["confirmed_to_completed"] + counts["active_to_completed"] print(f" Total updated : {total}") print("───────────────────────────────────────────────────────────────") client.close() if __name__ == "__main__": asyncio.run(run())