Files
touchbase/docs/progress/2026-05-01-schema-and-seed.md
2026-05-01 18:24:09 -04:00

9.1 KiB
Raw Blame History

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 test10/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 TueSat 10:0019: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 2025%
Default working hours (seed) TueSat 10:0019: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 timezoneAPP_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

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

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:0003: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.