Wait-List & Dynamic Re-Allocation (PL-T197)
At a glance
A FIFO wait-list with auto-promotion when slots free up — keyed per (resource_type, time_range). SMS-alert with configurable response deadline snapshotted per entry, race-safe token-based accept that 409s losers and auto-promotes the next candidate, position recompute on every queue change, supersede-and-requeue on maintenance, hourly metrics rollup for materialized wait-time estimates, customer-cancel-mid-promotion handling, and a tenant killswitch via WaitListPolicy.enabled.
How it works
When a booking attempt 409s on conflict, the customer can opt into the wait-list — a WaitListEntry is written into a FIFO bucket keyed on (resource_type, time_range), with an integer position field. When a booking in that bucket is cancelled or maintenance closes a slot, WaitListService.promote_for_freed_slot picks the head of the queue and creates a WaitListPromotion document with status pending. A waitlist_promoted event fires through the notifications hub — the candidate gets an SMS with a tokenized accept/decline link.
The promotion_response_window_minutes from the venue profile is snapshotted onto the entry at promotion time so policy changes don't disturb in-flight promotions. POST /chain/waitlist/promotions/{id}/accept (token-based, no login required) is race-safe: if two candidates somehow accept the same slot, the loser gets 409 slot_unavailable and the system auto-promotes the next. POST /decline records an optional reason.
The waitlist-expiration-tick cron runs every minute, expiring unaccepted promotions and promoting the next candidate. Position is recomputed FIFO on every create / cancel / promote so the queue never has gaps. handle_resource_unavailable supersedes affected promotions back to WAITING when maintenance is added retroactively. Customer cancel mid-promotion flips status to SUPERSEDED and offers the slot to the next candidate.
An hourly waitlist-position-metrics rollup materializes estimated_wait_minutes per bucket so customers see realistic ETAs before committing. SSE broadcasts on chain.waitlist.{tenant}.{location} and the per-entry channel chain.waitlist.entry.{id} keep the operator console and the customer's confirmation page live. Default sv/en SMS+email templates are seeded on first profile-create.
WaitListPolicy.enabled = false is a tenant killswitch that disables the entire feature without code changes.
Key capabilities
- FIFO queue per (resource_type, time_range) with auto-promote on cancellation
- WaitListPromotion status machine (pending → accepted/declined/expired/superseded) with token-based guest access
- Race-safe accept — concurrent winners get 409 + auto-promote next
- Position recompute on every create/cancel/promote + per-entry response-window snapshot
- Supersede + re-queue on maintenance + customer-cancel mid-promotion
- Hourly metrics rollup → materialized estimated_wait_minutes
- SSE broadcasts + tenant killswitch via WaitListPolicy.enabled
In practice
Saturday at 18:30 a customer tries to book Lane 4 for 19:30. The slot is full — 409 with a wait-list option. They opt in; a WaitListEntry is written at position 3 in the (court, 19:00-20:30) bucket and the page shows estimated wait 45 min based on the materialized rollup.
At 19:10 another booking cancels. The promotion engine picks position 1 (someone ahead of them), fires SMS "You're in! Reply YES within 10 minutes".
Five minutes pass with no reply; the cron expires that promotion, picks position 2 — same story. At 19:20 it reaches our customer; SMS arrives, they tap the accept link, the booking is created and they get an instant confirmation. Their page (still open in the background) updates via SSE — "You're in, head over" — and they walk in by 19:35.
Features in this subsystem
16| ID | Status | Features |
|---|---|---|
| F22.15.01 | Shipped | FIFO queue per resource type + time range (WaitListEntry.position) ✅ |
| F22.15.02 | Shipped | Auto-promote on cancellation (WaitListService.promote_for_freed_slot) ✅ |
| F22.15.03 | Shipped | SMS-alert with configurable response deadline (waitlist_promoted event) ✅ |
| F22.15.04 | Shipped | Cron job for expired deadlines (waitlist-expiration-tick, every minute) ✅ |
| F22.15.05 | Shipped | WaitListPromotion-dokument med statusmaskin pending → accepted/declined/expired/superseded ✅ |
| F22.15.06 | Shipped | POST /chain/waitlist/promotions/{id}/accept med token-baserad guest-access ✅ |
| F22.15.07 | Shipped | POST /chain/waitlist/promotions/{id}/decline med valfritt skäl ✅ |
| F22.15.08 | Shipped | Position-recompute vid create/cancel/promote (FIFO inom bucket) ✅ |
| F22.15.09 | Shipped | Snapshot av promotion_response_window_minutes per entry ✅ |
| F22.15.10 | Shipped | handle_resource_unavailable — supersede + retur till WAITING vid maintenance ✅ |
| F22.15.11 | Shipped | Race-säkert accept (concurrent → slot_unavailable 409 + auto-promote nästa) ✅ |
| F22.15.12 | Shipped | Hourly waitlist-position-metrics-rollup för materialiserad estimated_wait_minutes ✅ |
| F22.15.13 | Shipped | SSE-broadcast på chain.waitlist.{tenant}.{location} + chain.waitlist.entry.{id} ✅ |
| F22.15.14 | Shipped | Default-templates (sv/en × SMS/email) seedas vid första profile-create ✅ |
| F22.15.15 | Shipped | Tenant-killswitch via WaitListPolicy.enabled=False ✅ |
| F22.15.16 | Shipped | Customer cancel mid-promotion → SUPERSEDED + nästa kandidat erbjuds ✅ |