added availability
This commit is contained in:
168
docs/progress/2026-04-30-bootstrap.md
Normal file
168
docs/progress/2026-04-30-bootstrap.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 2026-04-30 — Bootstrap & Exclusion-Constraint Spike
|
||||
|
||||
> Snapshot at `2026-04-30T17:12Z`. Companion to `Initial.md` (planning) at `/Users/noise/Documents/obsidian/Massage/Initial.md`.
|
||||
|
||||
## Milestone
|
||||
|
||||
The five bootstrap tasks from the kickoff plan are complete. The repository is initialized, Postgres is running under docker-compose, the schema-managed migration includes the two `EXCLUDE USING gist` constraints that prevent therapist and room double-booking at the database level, and a Vitest suite of seven tests proves the constraints behave as specified.
|
||||
|
||||
This is the foundation everything else builds on per `Initial.md` §9 step 1.
|
||||
|
||||
## What's verified
|
||||
|
||||
- `pnpm install` — clean
|
||||
- `pnpm lint` — clean (0 warnings, 0 errors)
|
||||
- `pnpm exec tsc --noEmit` — clean
|
||||
- `pnpm test` — **7/7 tests green**
|
||||
- `docker-compose up -d postgres mailpit` — healthy on first or second try (see "Gotchas")
|
||||
- `pnpm exec prisma migrate dev` — applies cleanly; constraints visible via `\d "Booking"`
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
touchbase/
|
||||
├── caddy/Caddyfile # prod reverse proxy (profile: prod)
|
||||
├── compose.yaml # postgres + mailpit always; app + caddy under prod profile
|
||||
├── db/init/01-extensions-and-test-db.sql # creates touchbase_test, installs btree_gist + pgcrypto
|
||||
├── docs/progress/ # this dir — milestone snapshots
|
||||
├── prisma/
|
||||
│ ├── migrations/20260430170142_booking_spike/migration.sql
|
||||
│ ├── schema.prisma # Booking model only; full schema lands later
|
||||
│ └── ...
|
||||
├── scripts/db-bootstrap.sh # idempotent fallback for the bind-mount race
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router (untouched starter for now)
|
||||
│ ├── generated/prisma/ # generated, gitignored
|
||||
│ └── lib/db.ts # singleton PrismaClient with PrismaPg adapter
|
||||
├── test/
|
||||
│ ├── booking-exclusion.test.ts # 7 tests
|
||||
│ └── setup.ts # loads .env.test and refuses non-test DBs
|
||||
├── .env, .env.test, .env.example
|
||||
├── compose.yaml, Caddyfile (above)
|
||||
├── package.json, pnpm-workspace.yaml, .nvmrc
|
||||
├── prisma.config.ts, vitest.config.ts
|
||||
└── eslint.config.mjs, tsconfig.json, next.config.ts (defaults)
|
||||
```
|
||||
|
||||
## Decisions ratified this session
|
||||
|
||||
| Decision | Resolution |
|
||||
|---|---|
|
||||
| Project name | **TouchBase** |
|
||||
| Code path | `/Users/noise/Documents/code/touchbase/` (outside Obsidian vault) |
|
||||
| Email transport | SMTP via `nodemailer`. Dev: Mailpit (`localhost:1025`). Prod: Resend |
|
||||
| Email provider chosen between Resend and Postmark | **Resend** (assistant's call, not user-overridden) |
|
||||
| Node engines | `>=22`; `.nvmrc` pinned to `22` |
|
||||
| Package manager | pnpm 10 |
|
||||
| Currency | Assumed USD (`priceCents`) — pending user confirmation |
|
||||
| `APP_TZ` placeholder | `America/Los_Angeles` — pending user confirmation |
|
||||
|
||||
## Stack snapshot (resolved versions)
|
||||
|
||||
- Next.js 16.2.4 (App Router) + React 19.2.4 + Tailwind 4.2.4
|
||||
- Prisma 7.8.0 with the new `prisma-client` generator (TS-native client at `src/generated/prisma/`)
|
||||
- `@prisma/adapter-pg` 7.8.0 + `pg` 8.20.0 — required by Prisma 7 (no built-in connection)
|
||||
- Vitest 4.1.5 + dotenv-cli 9.0.0 + zod 4.3.6
|
||||
- Postgres 16-alpine with `btree_gist` and `pgcrypto` extensions
|
||||
- Mailpit (latest) for dev SMTP
|
||||
- Caddy 2-alpine for prod TLS/proxy
|
||||
|
||||
## Database state
|
||||
|
||||
Two databases on the single Postgres container:
|
||||
|
||||
- `touchbase_dev` — used by `pnpm dev` and `prisma migrate dev`
|
||||
- `touchbase_test` — used by `pnpm test`; reset between test files
|
||||
|
||||
Both have `btree_gist` and `pgcrypto` extensions installed. Migrations are applied to both via separate flows (`prisma migrate dev` for dev, `dotenv-cli -e .env.test -- prisma migrate deploy` for test).
|
||||
|
||||
Migration `20260430170142_booking_spike` creates:
|
||||
- `BookingStatus` enum: `HOLD | CONFIRMED | COMPLETED | NO_SHOW | CANCELLED`
|
||||
- `Booking` table (no FKs yet — opaque text IDs for `customerId`, `therapistId`, `roomId`, `serviceId`)
|
||||
- 4 btree indexes for query patterns we expect
|
||||
- **`Booking_no_therapist_overlap`** — `EXCLUDE USING gist` on `(therapistId, tstzrange(startsAt, endsAt))`, scoped to active statuses
|
||||
- **`Booking_no_room_overlap`** — same shape but on `(roomId, tstzrange(startsAt, roomReleasedAt))` so the post-service buffer is enforced
|
||||
|
||||
`roomReleasedAt = endsAt + service.bufferAfterMin`. Stored on the row so the constraint is single-table without a join.
|
||||
|
||||
## Test cases (all passing)
|
||||
|
||||
In `test/booking-exclusion.test.ts`:
|
||||
|
||||
1. Non-overlapping bookings on the same therapist succeed
|
||||
2. Overlapping bookings on the same therapist are rejected by the DB (raw SQLSTATE 23P01)
|
||||
3. Two therapists, same time slot, different rooms — both succeed
|
||||
4. Room buffer is enforced — second booking inside the buffer window is rejected
|
||||
5. A booking starting exactly when the room is released is allowed (range is `[)`)
|
||||
6. CANCELLED bookings do not block new bookings on the same slot
|
||||
7. HOLD blocks CONFIRMED in the same slot — race-condition safety net
|
||||
|
||||
## Gotchas hit and how they were resolved
|
||||
|
||||
### 1. Docker Desktop bind-mount race on first Postgres start
|
||||
The `/docker-entrypoint-initdb.d/*` glob expanded to the literal pattern (zero matches) the first time the container started, so init scripts were skipped — `touchbase_test` wasn't created and extensions weren't installed.
|
||||
|
||||
**Fix:** `docker-compose down -v` and re-up resolved it. As a permanent safety net, `scripts/db-bootstrap.sh` is idempotent and re-applies the same setup. Wired up as `pnpm db:bootstrap`. The README of the spike directory (this doc, until we have a real README) is the place this lives.
|
||||
|
||||
### 2. Prisma 7 dropped `datasourceUrl` and `datasources` constructor options
|
||||
`new PrismaClient({ datasourceUrl: ... })` — the pattern that worked through Prisma 6 — now throws `PrismaClientConstructorValidationError`. Prisma 7 requires a driver adapter.
|
||||
|
||||
**Fix:** added `@prisma/adapter-pg` and `pg`, and `PrismaClient` is now constructed as:
|
||||
|
||||
```ts
|
||||
new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) })
|
||||
```
|
||||
|
||||
Encapsulated in `src/lib/db.ts`. Worth noting that all training-data examples for Prisma will be wrong on this point — assume Prisma 7 docs are authoritative going forward.
|
||||
|
||||
### 3. Prisma 7 error shape doesn't surface SQLSTATE at top level
|
||||
`PrismaClientKnownRequestError` for an exclusion violation didn't expose `error.code === '23P01'` in any obvious place. The constraint name *is* present in `error.meta.constraint`.
|
||||
|
||||
**Fix:** `findPgCode` in `test/booking-exclusion.test.ts:47` walks the error and falls back to constraint-name presence. Loose — a foreign-key or unique violation would also be classified as 23P01 by this fallback. Acceptable for the spike; tighten before this helper is used outside tests.
|
||||
|
||||
### 4. Vitest 4 renamed `fileParallel` → `fileParallelism`
|
||||
Quiet runtime-OK but caught by `tsc --noEmit`.
|
||||
|
||||
**Fix:** updated `vitest.config.ts`.
|
||||
|
||||
### 5. Docker Compose plugin not installed
|
||||
The `docker compose` (space) form failed; `docker-compose` (hyphenated, v5.1.2 from Homebrew) works. All scripts and docs use the hyphenated form.
|
||||
|
||||
## Open questions for the user
|
||||
|
||||
These are non-blocking but matter before the public booking page lands:
|
||||
|
||||
1. **Practice timezone.** `APP_TZ` is a placeholder. The system clock observed is EDT during this session — but that may just be the developer location. What's the practice's local timezone?
|
||||
2. **Public-facing name.** Is "TouchBase" the brand customers will see, or just the codebase name? Affects email templates, page titles, etc.
|
||||
3. **Currency.** USD assumed. Confirm.
|
||||
4. **Stripe account ownership.** Personal vs. LLC vs. existing entity — flagged for when billing wiring lands (~week 4–5).
|
||||
|
||||
## Next step (recommended)
|
||||
|
||||
Build out the **full schema** per `Initial.md` §4 in one migration: User + Customer + Therapist + Room + Service + WorkingHours + AvailabilityOverride + tagging tables + RoomBlock + Notification + AuditLog + the FK retrofit for Booking. Then a seed script with 10 therapists, 10 rooms, ~5 services, ~10 tags. Then start on the **availability algorithm** as a pure-function module with a fixture-driven test suite — the load-bearing piece per `Initial.md` §5.
|
||||
|
||||
Auth.js and the customer-facing UI come after the algorithm is provably correct against fixtures.
|
||||
|
||||
## How to resume in a new session
|
||||
|
||||
```bash
|
||||
cd /Users/noise/Documents/code/touchbase
|
||||
docker-compose up -d postgres mailpit
|
||||
# If touchbase_test is missing or extensions not installed:
|
||||
pnpm db:bootstrap
|
||||
pnpm install
|
||||
pnpm test # should be 7/7 green
|
||||
pnpm dev # starts Next.js on http://localhost:3000 (still the create-next-app starter)
|
||||
```
|
||||
|
||||
Mailpit web UI: http://localhost:8025
|
||||
|
||||
## Files of interest for cross-reference
|
||||
|
||||
- Planning: `/Users/noise/Documents/obsidian/Massage/Initial.md`
|
||||
- Schema: `prisma/schema.prisma`
|
||||
- Constraint SQL: `prisma/migrations/20260430170142_booking_spike/migration.sql`
|
||||
- Tests: `test/booking-exclusion.test.ts`
|
||||
- DB singleton: `src/lib/db.ts`
|
||||
- Compose: `compose.yaml`
|
||||
- Bootstrap script: `scripts/db-bootstrap.sh`
|
||||
173
docs/progress/2026-05-01-schema-and-seed.md
Normal file
173
docs/progress/2026-05-01-schema-and-seed.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# 2026-05-01 — Full Schema + Seed
|
||||
|
||||
> Companion to `Initial.md` (planning) at `/Users/noise/Documents/obsidian/Massage/Initial.md`. Predecessor: `2026-04-30-bootstrap.md`.
|
||||
|
||||
## Milestone
|
||||
|
||||
The full data model from `Initial.md` §4 is in the database, the existing exclusion-constraint tests have been refactored to use real foreign-key-respecting fixtures from a new seed function, and three new tests prove the FK constraints reject orphan inserts. Seeded with realistic fixture data: 10 therapists with mixed qualifications, 10 rooms with mixed capabilities, 5 services covering the full tag-matching surface area.
|
||||
|
||||
This closes Step 2 of `Initial.md` §9 ("Schema + migrations + seed script"). Up next: the availability algorithm.
|
||||
|
||||
## What's verified
|
||||
|
||||
- `pnpm install` — clean
|
||||
- `pnpm lint` — clean
|
||||
- `pnpm exec tsc --noEmit` — clean
|
||||
- `pnpm test` — **10/10 green** (7 exclusion + 3 FK)
|
||||
- `pnpm db:seed` — seeds 1 admin + 10 therapists + 10 rooms + 5 services + 3 customers; idempotent via `TRUNCATE … CASCADE`
|
||||
- Schema visible via `\dt` on `touchbase_dev`: 15 tables (admin tables) + `_prisma_migrations`
|
||||
- Both exclusion constraints from the spike survived the schema migration alongside the new FK constraints — verified via `\d "Booking"`
|
||||
|
||||
## What's in the database (15 tables)
|
||||
|
||||
| Table | Notes |
|
||||
|---|---|
|
||||
| `User` | base identity; `role` enum drives which side relation populates |
|
||||
| `Customer` | extends User; `notes` field flagged for column-encryption before prod |
|
||||
| `Therapist` | extends User; relations to tags, working hours, overrides, services |
|
||||
| `TherapistTag` | composite PK; free-form tag strings |
|
||||
| `Room`, `RoomTag`, `RoomBlock` | symmetric to therapist side |
|
||||
| `Service` | `requiredTherapistTags` and `requiredRoomTags` as Postgres `text[]` |
|
||||
| `ServiceTherapist` | explicit allowlist of who-performs-what |
|
||||
| `WorkingHours` | minutes-from-midnight in **practice-local TZ**; effective-from/to for schedule changes |
|
||||
| `AvailabilityOverride` | BLOCK or EXTRA_HOURS variants |
|
||||
| `Booking` | retrofitted with FKs to User, Therapist, Room, Service; exclusion constraints intact |
|
||||
| `Payment` | linked to Booking via cascading FK |
|
||||
| `Notification` | snapshot semantics — no FK to User intentionally |
|
||||
| `AuditLog` | `actorId` is nullable for system actions |
|
||||
|
||||
## Migration history
|
||||
|
||||
```
|
||||
prisma/migrations/
|
||||
├── 20260430170142_booking_spike/ ← Booking + 2 exclusion constraints
|
||||
└── 20260430172029_full_schema/ ← all other tables + FK retrofit on Booking
|
||||
```
|
||||
|
||||
Both migrations replay cleanly on a reset (verified during the test-DB reset).
|
||||
|
||||
## Test coverage now (10 cases)
|
||||
|
||||
In `test/booking-exclusion.test.ts`:
|
||||
|
||||
**Exclusion constraints (7)**
|
||||
1. Non-overlapping bookings on the same therapist succeed
|
||||
2. Overlapping therapist bookings rejected (raw SQLSTATE 23P01)
|
||||
3. Two therapists, same time, different rooms succeed
|
||||
4. Room buffer enforced — second booking inside the buffer window rejected
|
||||
5. Booking starting exactly at room-released time succeeds (range is `[)`)
|
||||
6. CANCELLED bookings don't block new bookings
|
||||
7. HOLD blocks CONFIRMED in the same slot
|
||||
|
||||
**Foreign keys (3)**
|
||||
8. Non-existent `therapistId` rejected
|
||||
9. Non-existent `roomId` rejected
|
||||
10. Non-existent `customerId` rejected
|
||||
|
||||
## Seed snapshot
|
||||
|
||||
Verified via `psql`:
|
||||
|
||||
| Service | Required tags | Eligible therapists |
|
||||
|---|---|---|
|
||||
| 60-min Swedish | t:swedish | 10 |
|
||||
| 90-min Deep Tissue | t:deep-tissue | 4 |
|
||||
| 75-min Prenatal | t:prenatal-cert + r:prenatal-table | 3 |
|
||||
| 90-min Hot Stone | t:hot-stone-cert + r:hot-stone-equipped | 3 |
|
||||
| 60-min Lymphatic Drainage | t:lymphatic | 4 |
|
||||
|
||||
Therapists have realistic tag distributions (everyone does Swedish; specialty distributions vary). All therapists have default working hours Tue–Sat 10:00–19:00 in the practice TZ.
|
||||
|
||||
## Decisions made this session
|
||||
|
||||
| Decision | Resolution |
|
||||
|---|---|
|
||||
| Boundary semantics for booking overlaps | `[)` half-open. A booking at 11:00 may follow one ending at 11:00. |
|
||||
| Service buffer storage | Stored on `Booking.roomReleasedAt` so the room exclusion constraint is single-column |
|
||||
| Tag vocabulary | Free-form strings (no controlled-vocab table); admin UI manages vocabulary |
|
||||
| Service eligibility | Two-condition: (a) tag intersection AND (b) explicit `ServiceTherapist` allowlist row |
|
||||
| FK delete behavior | `Restrict` for Booking-side FKs (User, Therapist, Room, Service); `Cascade` for owned children (Payment, tags, working hours, etc.) |
|
||||
| Seed strategy | TRUNCATE-based wipe + insert. Idempotent. Same function used by CLI and tests |
|
||||
| Test seeding cadence | Once per file in `beforeAll`, then `db.booking.deleteMany()` per test |
|
||||
| TS execution for scripts | `tsx` (modern default; avoids ts-node config) |
|
||||
| Currency | USD assumed (`Service.priceCents`, `Payment.currency = "usd"`) — pending user confirmation |
|
||||
| Service prices (seed) | $90–$135; deposits 20–25% |
|
||||
| Default working hours (seed) | Tue–Sat 10:00–19:00 — practice-typical |
|
||||
|
||||
## Gotchas hit
|
||||
|
||||
### 1. Prisma 7 AI-agent guard on destructive migrate operations
|
||||
|
||||
`prisma migrate reset` (and presumably `db push --force-reset`, etc.) refuses to run when invoked by an AI agent without an explicit `PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION` env var carrying the literal text of the user's consenting message. The error message provides the exact protocol — communicate the action, motivation, destructive nature, prod/dev assessment, then ask. No known way to disable this guard, and probably shouldn't be disabled.
|
||||
|
||||
**Implication**: any future migration reset, schema reset, or db wipe needs a user confirmation round-trip. Worth knowing for session pacing.
|
||||
|
||||
### 2. Prisma 7 dropped `--skip-seed`
|
||||
|
||||
The `db:test:reset` script had `--skip-seed`, which Prisma 7 doesn't recognize. Removed.
|
||||
|
||||
### 3. Non-FK-respecting test fixtures break under FK retrofit
|
||||
|
||||
Tests previously used opaque text IDs ("therapist-A", "room-1") which worked when Booking had no FK. Once FKs were in place, those tests would fail with FK violations before reaching the constraint they were testing. Solved by seeding real records in `beforeAll` and reading their IDs into module-level variables.
|
||||
|
||||
## Open questions for the user
|
||||
|
||||
Still pending from prior session, all non-blocking but worth resolving before public launch:
|
||||
|
||||
1. **Practice timezone** — `APP_TZ=America/Los_Angeles` placeholder; system clock is EDT, but that may be developer-only.
|
||||
2. **Public-facing brand name** — is "TouchBase" customer-visible or internal-only?
|
||||
3. **Currency** — USD assumed.
|
||||
4. **Stripe account** — for when we wire billing.
|
||||
|
||||
## Repo state
|
||||
|
||||
```
|
||||
touchbase/
|
||||
├── compose.yaml, Caddyfile, db/init/
|
||||
├── prisma/
|
||||
│ ├── migrations/20260430170142_booking_spike/
|
||||
│ ├── migrations/20260430172029_full_schema/ ← NEW
|
||||
│ ├── schema.prisma ← FULL
|
||||
│ └── seed.ts ← NEW (CLI)
|
||||
├── scripts/db-bootstrap.sh
|
||||
├── src/
|
||||
│ ├── app/ (Next.js starter, untouched)
|
||||
│ ├── generated/prisma/ (gitignored)
|
||||
│ └── lib/
|
||||
│ ├── db.ts
|
||||
│ └── seed.ts ← NEW (function)
|
||||
├── test/
|
||||
│ ├── booking-exclusion.test.ts ← REFACTORED (now 10 tests)
|
||||
│ └── setup.ts
|
||||
├── docs/progress/
|
||||
│ ├── 2026-04-30-bootstrap.md
|
||||
│ └── 2026-05-01-schema-and-seed.md ← THIS DOC
|
||||
├── .env, .env.test, .env.example, .nvmrc
|
||||
├── package.json (added: db:seed; modified: db:test:reset)
|
||||
├── pnpm-workspace.yaml, prisma.config.ts, vitest.config.ts
|
||||
└── eslint.config.mjs, tsconfig.json, next.config.ts
|
||||
```
|
||||
|
||||
## How to resume in a new session
|
||||
|
||||
```bash
|
||||
cd /Users/noise/Documents/code/touchbase
|
||||
docker-compose up -d postgres mailpit
|
||||
pnpm install # if anything changed
|
||||
pnpm db:seed # reset + reseed dev DB (NB: TRUNCATEs everything)
|
||||
pnpm test # should be 10/10 green
|
||||
```
|
||||
|
||||
## Next step (recommended, starting immediately after this snapshot)
|
||||
|
||||
Per `Initial.md` §5: **the pure availability algorithm**. A function `findSlots(service, window, resourceState, options)` that returns slots with candidate therapist/room sets, given working hours, overrides, room blocks, existing bookings, and tag-matching constraints.
|
||||
|
||||
Key design choices going in:
|
||||
- **Pure**: takes resource state as input, no DB inside the function. A separate loader does the DB hit. This makes the algorithm trivially testable with fixtures.
|
||||
- **TZ-aware**: working hours are wall-clock-local; bookings are UTC. We use `date-fns-tz` (Temporal isn't reliably available on Node yet).
|
||||
- **Output shape**: each slot returns `{ startsAt, endsAt, candidateTherapists, candidateRooms }`. Greedy assignment happens at booking-commit time, not in the slot search.
|
||||
- **DST**: spring-forward day specifically — local 02:00–03:00 doesn't exist, so working hours that span it lose an hour.
|
||||
- **Slot granularity**: configurable, default 15 min. Not service-aligned.
|
||||
- **Window**: default 30 days forward.
|
||||
|
||||
Tests should cover: working-hours match/miss, BLOCK overrides, EXTRA_HOURS overrides, room blocks, room buffer collisions, therapist tag mismatch, room tag mismatch, ServiceTherapist allowlist exclusion, therapist preference filter, empty result, DST day.
|
||||
Reference in New Issue
Block a user