Architecture · options · RFC #403 · for discussion

BenzAuto — Availability & bay-management: four candidate engines

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.

A · Patch the current fixed 30-min slot grid

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.

B · Interval-based capacity planner (rule-driven)

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.

C · Constraint solver (OR-Tools CP-SAT)

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.

D · Drop-off / day-bucket capacity model

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.

Side by side

Same problem (bays × time × job duration, constrained by hours and policy), four shapes of answer. Effort is relative to the current codebase.

OptionModelBooking UXEffortBest 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)

Shared per-tenant policy config

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 decision, on three axes

The four options aren't a ranking — they're points on three independent axes. Pick a position on each and the option falls out.

Exact start time A and C anchor on exact times · B offers windows · D drops start times entirely Day / AM-PM bucket
Rule-based A, B, D are rule-driven · C is the only constraint solver Constraint solver
Build (our stack) A, B, D stay in Bun/TS · C buys into OR-Tools + a separate runtime · D adopts the Tekmetric / Shopmonkey model Buy / adopt a model

Full write-up and discussion: issue #403 — availability & bay-management engine (RFC).