Where the booking engine could go from the current fixed-grid loop. Four candidates (A–D), each with a flow sketch and a pros / cons / effort line, a side-by-side comparison, the shared per-tenant policy config they all read, and the three axes the decision turns on. Pair this with the current-state diagram.
Keep the existing forward-walking 30-min loop, but stop after collecting all fitting slots instead of the first three. Add a time-of-day filter, apply the per-tenant reserve/cap config as post-filters, and pass the current time into the AI. Smallest possible change that closes the live failures.
flowchart TD A(["check_availability
date · timeOfDay · now"]) --> B["Walk 30-min grid
(existing loop)"] B --> C["Collect ALL fitting slots
not just the first 3"] C --> D{"time-of-day
filter set?"} D -- yes --> E["keep AM / PM / evening"] D -- no --> F["post-filter:
reserve + caps
per-tenant config"] E --> F F --> G["spread across the day
sample + 'later' paging"] G --> Z(["return slots + reason"])
Effort S Pros smallest change; ships on today's engine; directly closes G1 (limit-3) and G2 (no time-of-day); no new service. Cons reserve/caps are bolt-on post-filters, not first-class; still can't model variable duration or equipment. Ceiling still a rigid grid.
Model each bay as a timeline of free intervals. Place a job of duration[min,max] + buffer into a bay that has the right equipment, then run a per-tenant policy layer (reserve-bays, reserve-hours, caps, equipment). Emit offer windows that staff confirm. Stays entirely in our Bun/TypeScript stack.
flowchart TD A(["request: services + date
timeOfDay?"]) --> B["Build per-bay timeline
of FREE intervals"] B --> C["Job = duration[min,max]
+ buffer"] C --> D["Fit job into intervals
(equipment-aware bay)"] D --> E["Policy layer:
reserve-bays · reserve-hours
caps · equipment"] E --> F["Emit candidate WINDOWS"] F --> Z(["offer windows -> staff confirm"])
Effort M Pros real per-bay scheduling; handles variable duration + buffer + equipment; clean per-tenant policy layer; explainable; stays in Bun/TS. Cons more code than A; we own the interval-fitting and policy logic; windows UX needs a staff-confirm step. Recommended middle ground.
Jobs become interval variables, bays become resources, policy becomes constraints, and a solver finds a feasible (or optimal) assignment. Most likely a separate Python/native service the TypeScript layer calls. The most powerful and future-proof option — it generalises to technicians and skills — but the heaviest to run and explain.
flowchart TD A(["request: services + date"]) --> B["TS service builds model"] B --> C[/"Python CP-SAT service"/] C --> D["jobs = interval vars
bays = resources"] D --> E["policy = constraints
reserve · caps · skills"] E --> F{"feasible?"} F -- yes --> G(["optimal slots / windows"]) F -- no --> H(["reason: infeasible set"])
Effort L Pros most powerful and future-proof (technicians, skills, optimisation); provably optimal assignments. Cons likely a separate Python/native service; heavy ops; added latency; hard to explain "why no slot" to a customer. Future-proof but biggest jump.
No exact start times. Sell capacity per day, or per AM/PM, by job class (light / standard / heavy), capped, with reserved headroom held back for walk-ins. This is the model drop-off shops (Tekmetric, Shopmonkey) ship. The customer leaves the car; the shop sequences the work.
flowchart TD A(["request: services + date"]) --> B["Classify job:
light · standard · heavy"] B --> C["Read day buckets:
per-day / AM / PM caps"] C --> D["Subtract booked
+ reserved walk-in headroom"] D --> E{"capacity left
in the bucket?"} E -- yes --> F(["offer 'drop off AM / PM'"]) E -- no --> G(["offer next open day"])
Effort S–M Pros dead-simple UX ("leave the car"); robust to estimate error; easy caps + walk-in headroom; proven pattern. Cons no exact start time; wrong for customers who want a specific time; coarse capacity view. Best fit for "leave the car" shops.
Same problem (bays × time × job duration, constrained by hours and policy), four shapes of answer. Effort is relative to the current codebase.
| Option | Model | Booking UX | Effort | Best for |
|---|---|---|---|---|
| A | Patched fixed 30-min grid | Exact slots, spread across day + time-of-day filter | S | Shipping a fix now on the existing engine |
| B | Interval capacity planner (rules) | Offer windows, staff-confirm | M | Realistic per-bay scheduling that stays in our stack |
| C | CP-SAT constraint solver | Optimal slots / windows | L | Long term: technicians, skills, complex constraints |
| D | Day / AM-PM capacity buckets | "Leave the car", no exact time | S–M | Drop-off shops (Tekmetric / Shopmonkey style) |
Every option reads the same per-tenant configuration block — only how deeply it uses each field differs (A treats reserve/caps as post-filters; B/C make them first-class; D leans on caps + reserve). One shape, stored on the organization, keeps the workshops portable across whichever engine we pick.
bayManagement: { bays: [ { name: "Bay 1", equipment: ["lift", "aircon"], active: true }, { name: "Bay 2", equipment: ["lift"], active: true }, { name: "Bay 3", equipment: [], active: true }, { name: "Bay 4", equipment: ["alignment"], active: false } ], // per service: class drives caps; duration is a range; buffer pads the bay services: { "oil-change": { class: "light", duration: { min: 30, max: 45 }, buffer: 10 }, "brake-service": { class: "standard", duration: { min: 60, max: 90 }, buffer: 15 }, "engine-overhaul": { class: "heavy", duration: { min: 240, max: 480 }, buffer: 30 } }, // hold capacity back from online booking reserve: { keepBaysOpen: 1, // always leave N bays for walk-ins reserveHoursPct: 20, // hold back 20% of bay-hours per day releaseAfter: "T-2h" // free the reserve 2h before close }, caps: { heavyPerDay: 2, majorPerDay: 4 }, bookingMode: "windows", // slots | windows | dropoff requireBayEquipment: true // only place a job in a bay with its required equipment }
The four options aren't a ranking — they're points on three independent axes. Pick a position on each and the option falls out.
Full write-up and discussion: issue #403 — availability & bay-management engine (RFC).