Release expired room holds

Python · Django · intermediate · modification

Abandoned bookings no longer block rooms: `expire_holds` flips every `HELD` hold past its `expires_at` to `EXPIRED` in one `UPDATE`, so it is cheap to run every minute, and the payment flow's `status=HELD` check keeps it safe against a guest confirming at the same moment. Added it to the crontab next to the other jobs. Tested locally: a hold created at 10:00:00 was still held at the 10:14 run and released at the first run after 10:15.

Django 5.1 booking site for a US hotel group on PostgreSQL. `settings.py` has `USE_TZ = True` and `TIME_ZONE = "America/New_York"`, the hotels' time zone, used for display. Production runs in containers whose system time zone is UTC (the crontab header says so); developers run the app on laptops set to New York time. `Hold` (existing, `bookings/models.py`) has `status` (`Hold.Status`: `HELD`, `CONFIRMED`, `EXPIRED`) and `expires_at = models.DateTimeField()`. `bookings/management/commands/` already exists (it holds `import_rates.py`).

Requirements

Files touched

--- bookings/management/commands/expire_holds.py
+from datetime import datetime
+
+from django.core.management.base import BaseCommand
+
+from bookings.models import Hold
+
+
+class Command(BaseCommand):
+    help = "Release room holds whose payment window has passed."
+
+    def handle(self, *args, **options):
+        now = datetime.now()
+        count = Hold.objects.filter(
+            status=Hold.Status.HELD,
+            expires_at__lte=now,
+        ).update(status=Hold.Status.EXPIRED)
+        self.stdout.write(f"Released {count} expired hold(s)")
+
--- deploy/crontab
 15 3 * * *  cd /app && python manage.py import_rates
 */10 * * * *  cd /app && python manage.py clearsessions
+* * * * *  cd /app && python manage.py expire_holds
 

Review this PR

Python practice