added availability
This commit is contained in:
88
docs/progress/2026-05-01-availability.md
Normal file
88
docs/progress/2026-05-01-availability.md
Normal 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.
|
||||
Reference in New Issue
Block a user