The written decision behind the two diagrams. Read this next to the current architecture (how booking works today and where it fails) and the four candidate engines (A–D, the shared policy config, the axes). Issue #403. Pilot floor-ops source of truth: KL Chan.
Four facts pulled from the live database. They rule out "just add more capacity" and point straight at engine logic as the failure.
General Service, Bay 3 = Tyre & Alignment (a stray "ZZ APA Bay 9" = test junk, to clean up). The bays table already HAS a bay_type column, but the engine picks bays.find(b => !busy) — bay_type is never read. A tyre job can land in a general bay and vice-versa.estimated_duration_min is wrong; we need a min/max range + buffer, and a multi-day path.service_items.max_per_day and booking_windows exist on every row but sit null. The code already enforces both when present — nothing populates them yet.The whole "do we have a slot?" decision lives in one function (packages/db/src/services/slot-proposal.ts). Here is everything it gets wrong.
bay_type — no equipment matching (tyre job ≠ tyre bay).Same problem (bays × time × job length, constrained by hours and policy), four shapes of answer. Full flow sketches live in the options diagram.
Keep today's forward-walking loop but collect all fitting slots (not the first 3), add a time-of-day filter, apply reserve/caps as post-filters, match bay_type.
Effort S Pros smallest change; closes the live "afternoon" / "next week" failures on today's engine.
Cons reserve/caps are bolt-ons; still can't model variable duration cleanly. Ceiling still a rigid grid.
Model each bay as a timeline of free intervals. Place a job of duration[min,max] + buffer into an equipment-matched bay, then run a per-tenant policy layer (reserve-bays, reserve-hours, caps). Emit slots or windows, staff-confirm.
Effort M Pros real per-bay scheduling; variable duration + buffer + equipment; clean policy layer; explainable; stays in Bun/TypeScript.
Cons more code than A; we own the interval-fitting logic. The recommended middle ground.
Jobs = time intervals, bays = resources, policy = constraints; a solver finds a feasible/optimal assignment. Generalises to technicians and skills. Likely a separate Python service.
Effort L Pros most powerful, future-proof; provably optimal.
Cons separate runtime; heavy ops; added latency; hard to explain "why no slot" to a customer. Biggest jump — hold in reserve.
No exact start times. Sell capacity per day or per AM/PM by job class (light / standard / heavy), capped, with headroom held back for walk-ins. The Tekmetric / Shopmonkey model.
Effort S–M Pros dead-simple "leave the car" UX; robust to estimate error; easy caps + walk-in headroom.
Cons no exact start time; wrong for customers who want "2pm sharp". Best fit for drop-off shops.
| Option | Model | Booking UX | Effort | Best for |
|---|---|---|---|---|
| A | Patched fixed 30-min grid | Exact slots, spread + time-of-day filter | S | Shipping a fix now on today's engine |
| B | Interval capacity planner (rules) | Offer windows / slots, staff-confirm | M | Realistic per-bay scheduling, in our stack |
| C | CP-SAT constraint solver | Optimal slots / windows | L | Long term: technicians, skills, optimisation |
| D | Day / AM-PM capacity buckets | "Leave the car", no exact time | S–M | Drop-off shops |
Every option reads the same per-org configuration. A treats reserve/caps as post-filters; B/C make them first-class; D leans on caps + reserve. One shape keeps workshops portable across whichever engine wins.
bayManagement: { bays: [ { name, bayType/equipment[], active } ] # bay_type EXISTS today services: # per service → { class: from service_type, duration:{min,max}, buffer, multiDay? } reserve: { keepBaysOpen: 1, reserveHoursPct: 0, releaseAfter: "15:00" } # walk-in protection caps: { heavyPerDay, majorPerDay } # max_per_day exists, unused bookingMode: slots | windows | dropoff requireBayEquipment: true # match job → bay_type }
The four options aren't a ranking — they're points on three independent axes. Pick a position on each and the option falls out.
service_type gives a hint; is the real length known at booking, or only after a diagnosis? (→ book Inspection first, schedule the heavy work after.)bookingMode)?