added availability

This commit is contained in:
2026-05-01 18:32:01 -04:00
parent 036512f590
commit b513accdf5

View File

@@ -0,0 +1,88 @@
# 2026-05-01 — Availability Algorithm
> Same-day successor to `2026-05-01-schema-and-seed.md`.
## Milestone
The pure availability algorithm from `Initial.md` §5 is implemented and exhaustively tested. Closes Step 3 of `Initial.md` §9.
## What landed
- `src/lib/availability.ts` — pure module exporting `findSlots`, `therapistAvailable`, `roomAvailable`, `inWorkingHours` plus the `*State` types the algorithm consumes.
- `test/availability.test.ts`**28 tests** across `inWorkingHours`, `therapistAvailable`, and `findSlots` integration.
## What's verified
- `pnpm test`**38/38 green** (10 schema + 28 availability)
- `pnpm lint` — clean
- `pnpm exec tsc --noEmit` — clean
## Coverage of the algorithm
`inWorkingHours` (6): inside hours, before-open, exactly-at-close, non-working-day, DST spring-forward day, `effectiveFrom` bracket, cross-midnight rejected.
`therapistAvailable` (4): free in hours, booking-overlap blocks, BLOCK override blocks, EXTRA_HOURS opens out-of-hours.
`findSlots` integration (18): baseline grid math, empty therapist/room lists, therapist tag mismatch, room tag mismatch, ServiceTherapist allowlist, inactive therapist/room, BLOCK PTO, room buffer enforcement, room block, preferred-therapist filter, off-grid `from` rounds up, out-of-hours window, DST day, slot ordering, partial-availability slot still emitted with surviving candidates.
## Decisions ratified
| Decision | Resolution |
|---|---|
| Pure / impure boundary | Algorithm is pure; DB loader is separate (not yet written) |
| Slot grid orientation | UTC, not local. DST handled implicitly |
| Slot granularity | Default 15 min; configurable via `slotGranularityMin` |
| Cross-midnight slots | Rejected outright |
| EXTRA_HOURS vs working hours | Union — slot is OK if either covers it |
| BLOCK vs everything | Always wins — any overlap kills the slot |
| Booking overlap semantics | Half-open `[)`, matching the exclusion constraints |
| TZ extraction library | `Intl.DateTimeFormat` (not `date-fns-tz` `toZonedTime`) — see Gotcha 1 |
| Output shape | Slot returns *all* candidate therapists/rooms; assignment happens at commit time |
## Gotchas hit
### 1. `date-fns-tz` v3 `toZonedTime` is system-TZ-dependent
`toZonedTime(date, tz)` returns a `Date` whose value is shifted such that `getHours()` (system-local) reads as the target zone's wall clock — but **only if you call the system-local accessors**, and only if you understand the shift. Calling `.getUTCHours()` on the result gives UTC, not the target. On an EDT developer machine targeting `America/New_York`, the call is a no-op and the bug is silent.
**Fix**: use `Intl.DateTimeFormat` with `formatToParts` directly. Cached per zone; ~50 LOC helper. Portable across system timezones, no library quirks.
`date-fns-tz` is still installed; we'll use `formatInTimeZone` for display formatting later (which uses Intl internally and is fine).
### 2. `Intl.DateTimeFormat` returns `"24"` for midnight in some locales with `hour12: false`
Quirky and well-documented. Normalized to `0` in the helper.
## Repo deltas this session
```
src/lib/availability.ts NEW ~250 LOC
test/availability.test.ts NEW ~330 LOC
package.json + date-fns + date-fns-tz, + tsx
prisma/schema.prisma (full schema from earlier today)
prisma/migrations/ (full_schema migration from earlier today)
prisma/seed.ts, src/lib/seed.ts (from earlier today)
test/booking-exclusion.test.ts (refactored to fixtures, +3 FK tests)
docs/progress/ 2 new snapshots (this and schema-and-seed)
```
## Open questions still unresolved
1. Practice timezone (`APP_TZ` placeholder)
2. Customer-visible brand name (TouchBase or other)
3. Currency
4. Stripe account ownership
## Roadmap status (per `Initial.md` §9)
1. ~~Spike: docker-compose + exclusion-constraint migration + tests~~ **done 2026-04-30**
2. ~~Schema + migrations + seed~~ **done 2026-05-01**
3. ~~Pure availability algorithm + tests~~ **done 2026-05-01**
4. **next**: DB loader for the algorithm + first end-to-end story (admin creates booking on behalf of customer, confirmation email sent)
5. Public self-booking → Stripe deposits → reminders
## Recommended next step
**(a)** DB loader — `loadAvailabilityState(db, { from, to, serviceId })` that turns DB state into the `TherapistState[]` / `RoomState[]` the algorithm consumes. Roughly half a day. Unblocks any UI work that needs real availability data.
Then the first end-to-end story (Step 4) becomes very close — admin picks slot via `findSlots`, calls a `createHold` server action that inserts a Booking with HOLD status, transactional with payment-intent issuance later. Auth.js + UI can stay deferred since admin-only flow doesn't need public auth yet.