diff --git a/docs/coaching.md b/docs/coaching.md index 2448369..3d76980 100644 --- a/docs/coaching.md +++ b/docs/coaching.md @@ -1,32 +1,42 @@ # Coaching Sessions Table -One-on-one sessions booked between a user and a coach. Linked to a `coaching_session`-type entry in `services`. +One-on-one sessions booked between a user and a coordinator. Each session is linked +to a `private_lessons`-type entry in `services`. ```mermaid erDiagram coaching_sessions { uuid id PK uuid service_id FK - uuid coach_id FK + uuid coordinator_id FK uuid user_id FK + uuid child_id FK "nullable; null means adult registration" timestamp scheduled_at "null until a slot is confirmed" - int duration_minutes - session_status status "pending | confirmed | cancelled | completed" + session_status status "awaiting_payment | pending | confirmed | cancelled | completed" text meeting_url text notes jsonb selected_time_slots "array of {start, end} objects" + text stripe_order_id "unique" timestamp created_at timestamp updated_at } - profiles ||--o{ coaching_sessions : "coach leads" + profiles ||--o{ coaching_sessions : "coordinator leads" profiles ||--o{ coaching_sessions : "user attends" services ||--o{ coaching_sessions : "fulfilled by" + children |o--o{ coaching_sessions : "registered for" ``` ## Notes -- `scheduled_at` is null by default — it is set once the user selects a specific slot from `selected_time_slots`. -- `selected_time_slots` is a JSON array of `{ start, end }` objects (ISO 8601 strings) representing the time options offered to the user e.g. `[{ "start": "2026-04-14T14:00:00Z", "end": "2026-04-14T17:00:00Z" }]`. -- `meeting_url` is provided by the coach after confirmation. +- `coordinator_id` references the `profiles` row of the coordinator leading the session; + `user_id` is the attending user. `child_id` is set when the session is booked on behalf + of a child (null for adult registrations). +- `scheduled_at` is null by default — it is set once a specific slot is confirmed from + `selected_time_slots`. +- `selected_time_slots` is a **required** JSON array of `{ start, end }` objects (ISO 8601 + strings) representing the availability windows the user offered when requesting the + session, e.g. `[{ "start": "2026-04-14T14:00:00Z", "end": "2026-04-14T17:00:00Z" }]`. +- `meeting_url` is provided after the session is confirmed. +- `stripe_order_id` links the session to its Stripe payment (unique). - `status = completed` is set after the session ends. diff --git a/docs/profiles.md b/docs/profiles.md index bf40442..ecff18c 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -1,6 +1,6 @@ # Profiles Table -Mirrors Supabase `auth.users` — populated via a database trigger on signup. Stores display data and the user's role within the platform. The last_login_at property is updated via a Server Action every time a user logs in. +Mirrors Supabase `auth.users` — populated via a database trigger on signup. Stores display data, contact details, and the user's role within the platform. `last_login_at` is updated via a Server Action every time a user logs in. ```mermaid erDiagram @@ -8,15 +8,21 @@ erDiagram uuid id PK "references auth.users(id)" text first_name text last_name - text stripe_customer_id "unique" - role role "user | admin | coach" + role role "user | admin | coordinator" + text address "nullable" + gender gender "nullable; male | female | prefer_not_to_say" + date dob "nullable" + text phone "nullable" + text stripe_customer_id "unique, nullable" + timestamp last_login_at timestamp created_at timestamp updated_at - timestamp last_login_at } ``` ## Notes - `id` is **not** auto-generated — it is set to the corresponding `auth.users.id` from Supabase Auth. -- `role` controls access: `user` is a regular member, `coach` can manage and lead sessions, `admin` has full access. +- `role` controls access (defaults to `user`): `user` is a regular member, `coordinator` can manage and lead services/coaching sessions, `admin` has full access. Values are also defined in `lib/roles.ts`. +- `first_name` and `last_name` are required; `address`, `gender`, `dob`, and `phone` are optional profile details. +- `stripe_customer_id` links the profile to its Stripe customer once created; it is unique. diff --git a/docs/schema-overview.md b/docs/schema-overview.md index 7c8379a..e22c5e4 100644 --- a/docs/schema-overview.md +++ b/docs/schema-overview.md @@ -3,11 +3,15 @@ ```mermaid erDiagram profiles { - uuid id PK + uuid id PK "references auth.users(id)" text first_name text last_name role role - text stripe_customer_id + text address + gender gender + date dob + text phone + text stripe_customer_id "unique" timestamp last_login_at timestamp created_at timestamp updated_at @@ -25,13 +29,14 @@ erDiagram service_type type date start_date "null for private_lessons" date end_date "null for private_lessons" - jsonb slots "array of {dayOfWeek, time}; null for private_lessons" + jsonb slots "ProgramSlot[] = {dayOfWeek, time}; null for private_lessons" int duration_minutes text stripe_product_id service_status status - uuid coach_id FK "null for programs" - uuid form_id FK "nullable" + uuid coordinator_id FK "required for private_lessons (see constraint)" + uuid form_id FK "nullable; set null on form delete" boolean is_for_children + boolean requires_subscription timestamp created_at timestamp updated_at } @@ -44,7 +49,7 @@ erDiagram booking_status status text notes boolean is_active - text stripe_order_id + text stripe_order_id "unique" timestamp created_at timestamp updated_at } @@ -64,26 +69,23 @@ erDiagram coaching_sessions { uuid id PK uuid service_id FK - uuid coach_id FK + uuid coordinator_id FK uuid user_id FK uuid child_id FK "nullable; null means adult registration" - timestamp scheduled_at "set when slot is confirmed" + timestamp scheduled_at "set when a slot is confirmed" session_status status text meeting_url text notes - jsonb selected_time_slots "array of {start, end} objects" - jsonb coach_time_slots - text coach_token - text client_token - text stripe_order_id + jsonb selected_time_slots "array of {start, end} ISO 8601 objects" + text stripe_order_id "unique" timestamp created_at timestamp updated_at } subscriptions { uuid id PK - uuid user_id FK - text stripe_subscription_id + uuid user_id FK "unique" + text stripe_subscription_id "unique" text status text stripe_price_id boolean cancel_at_period_end @@ -97,7 +99,7 @@ erDiagram uuid id PK uuid user_id FK text stripe_price_id - text stripe_session_id + text stripe_session_id "unique" text product_name int amount text currency @@ -135,7 +137,7 @@ erDiagram uuid form_id FK form_question_type type text prompt - jsonb options + jsonb options "FormQuestionOption[] = {id, title, description?}" int sort_order timestamp created_at timestamp updated_at @@ -153,7 +155,7 @@ erDiagram profiles ||--o{ service_bookings : "books" services ||--o{ service_bookings : "booked via" children |o--o{ service_bookings : "registered for" - profiles ||--o{ coaching_sessions : "coaches" + profiles ||--o{ coaching_sessions : "coordinates" profiles ||--o{ coaching_sessions : "attends" services ||--o{ coaching_sessions : "fulfilled by" children |o--o{ coaching_sessions : "registered for" @@ -161,28 +163,78 @@ erDiagram children ||--o{ emergency_contacts : "has" profiles ||--o| subscriptions : "has" profiles ||--o{ purchases : "makes" - profiles |o--o{ services : "coaches" + profiles |o--o{ services : "coordinates" forms ||--o{ form_questions : "contains" services }o--o| forms : "uses" form_questions ||--o{ form_question_answers : "answered via" children ||--o{ form_question_answers : "submits" ``` -## Indexes +## Per-table docs -| Table | Index | Type | Condition | -|---|---|---|---| -| `service_bookings` | `service_bookings_service_id_child_id_idx` | Unique (partial) | `WHERE child_id IS NOT NULL` — prevents the same child from registering for the same program twice | +| Table(s) | Doc | +|---|---| +| `profiles` | [profiles.md](./profiles.md) | +| `services`, `service_bookings` | [services.md](./services.md) | +| `coaching_sessions` | [coaching.md](./coaching.md) | +| `webinars` | [webinars.md](./webinars.md) | + +Tables without a dedicated doc (`forms`, `form_questions`, `form_question_answers`, +`children`, `emergency_contacts`, `subscriptions`, `purchases`) are covered by the +ER diagram above. ## Enums | Enum | Values | |---|---| -| `role` | `user`, `admin`, `coach` | +| `role` | `user`, `admin`, `coordinator` | | `service_type` | `private_lessons`, `programs` | -| `service_status` | `active`, `disabled`, `archived`, `deleted` | +| `service_status` | `active`, `archived`, `deleted`, `disabled` | | `booking_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled` | | `session_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled`, `completed` | | `webinar_tier` | `free`, `premium` | | `gender` | `male`, `female`, `prefer_not_to_say` | | `form_question_type` | `text`, `multiple_choices`, `checkboxes`, `user_agreement` | + +## Constraints + +| Table | Constraint | Rule | +|---|---|---| +| `services` | `services_private_lessons_require_coordinator` (CHECK) | A `private_lessons` service must have a `coordinator_id`. Other types may leave it null. | + +## Indexes + +| Table | Index | Type | Condition | +|---|---|---|---| +| `service_bookings` | `service_bookings_service_id_child_id_idx` | Unique (partial) | `WHERE child_id IS NOT NULL` — prevents the same child from registering for the same service twice | + +## JSONB shapes + +Some `jsonb` columns store typed structures defined in `lib/db/schema.ts`: + +| Column | Shape | Notes | +|---|---|---| +| `services.slots` | `ProgramSlot[]` — `{ dayOfWeek: number; time: string }` | Recurring weekly slots for `programs`; null for `private_lessons`. | +| `coaching_sessions.selected_time_slots` | `{ start: string; end: string }[]` | ISO 8601 windows the user offered when requesting a session. Not `$type`-annotated in the schema. | +| `form_questions.options` | `FormQuestionOption[]` — `{ id: string; title: string; description?: string }` | Choices for `multiple_choices` / `checkboxes` questions; null for other types. | + +## Working with the schema + +- **`lib/db/schema.ts` is the single source of truth.** The Drizzle config + (`drizzle.config.ts`) points at it, and all app code imports from it. This doc, + the ER diagram, and the migration files are all derived from it. +- **Naming convention:** columns are `snake_case` in Postgres and `camelCase` in + the Drizzle/TypeScript layer (e.g. `coordinator_id` ↔ `coordinatorId`). Keep both + in sync when adding columns. +- **Changing the schema — two workflows:** + - `pnpm db:push` — applies `schema.ts` directly to the database. Fast, good for + local prototyping; does **not** create a migration file. + - `pnpm db:generate` then `pnpm db:migrate` — generates a versioned SQL migration + under `drizzle/` and applies it. Use this for changes that ship to shared/prod + environments. + - Pick one workflow per change; don't run `db:push` and `db:migrate` against the + same environment expecting them to reconcile. +- `pnpm db:studio` opens Drizzle Studio to inspect data. +- **Keep the docs in sync:** the ER diagram and enum/constraint tables above are + hand-maintained. Update them (and the relevant per-table doc) in the same PR as + any `schema.ts` change. diff --git a/docs/services.md b/docs/services.md index 0f13214..43a5a3a 100644 --- a/docs/services.md +++ b/docs/services.md @@ -2,29 +2,38 @@ ## Services -The central catalog of offerings on the platform. Two types: -- **`booking`** — a fixed event with predetermined time slots set by the admin. Users buy it like a product with no input on timing. `scheduled_at` holds the time slots as a JSON array. -- **`coaching_session`** — a coaching offering where the user proposes availability. `scheduled_at` is null — timing is handled through the `coaching_sessions` table. +The central catalog of offerings on the platform. Two types (`service_type`): + +- **`private_lessons`** — one-on-one coaching led by a coordinator. The user proposes availability and scheduling is handled through the `coaching_sessions` table. A coordinator is **required** (enforced by a check constraint). +- **`programs`** — recurring group offerings with a fixed schedule. `slots` holds the recurring weekly times, and `start_date`/`end_date` bound the program. ```mermaid erDiagram services { uuid id PK - text title - text description - service_type type "coaching_session | booking" - jsonb scheduled_at "array of {start, end} objects — null for coaching_session" + service_type type "private_lessons | programs" + date start_date "null for private_lessons" + date end_date "null for private_lessons" + jsonb slots "ProgramSlot[] = {dayOfWeek, time}; null for private_lessons" int duration_minutes - int price "in cents" - boolean is_active + text stripe_product_id + service_status status "active | archived | deleted | disabled" + uuid coordinator_id FK "required for private_lessons" + uuid form_id FK "nullable" + boolean is_for_children + boolean requires_subscription timestamp created_at timestamp updated_at } + + profiles |o--o{ services : "coordinates" + services }o--o| forms : "uses" ``` ## Service Bookings -A user's purchase of a booking-type service. +A user's enrollment in a service. For `programs` this represents a seat; for +`private_lessons` the scheduling detail lives in `coaching_sessions`. ```mermaid erDiagram @@ -32,18 +41,36 @@ erDiagram uuid id PK uuid user_id FK uuid service_id FK - booking_status status "pending | confirmed | cancelled" + uuid child_id FK "nullable; null means adult registration" + booking_status status "awaiting_payment | pending | confirmed | cancelled" text notes boolean is_active + text stripe_order_id "unique" timestamp created_at timestamp updated_at } + profiles ||--o{ service_bookings : "books" services ||--o{ service_bookings : "booked via" + children |o--o{ service_bookings : "registered for" ``` ## Notes -- `price` is stored in **cents** (integer) to avoid floating-point issues. -- `is_active = false` hides a service without deleting historical bookings. -- `scheduled_at` is a JSON array of `{ start, end }` ISO 8601 objects e.g. `[{ "start": "2026-04-15T14:00:00Z", "end": "2026-04-15T16:00:00Z" }]`. +- **`coordinator_id` is required for `private_lessons`** — enforced by the + `services_private_lessons_require_coordinator` check constraint. It may be null + for `programs`. Deleting a referenced coordinator is restricted (`onDelete: restrict`). +- `slots` is a JSON array of `ProgramSlot` objects (`{ dayOfWeek: number; time: string }`) + describing the recurring weekly schedule of a program. It is null for `private_lessons`. +- `stripe_product_id` links the service to its Stripe product (required). Pricing lives + in Stripe, not in this table. +- `status` controls visibility/lifecycle: `active` is live; `disabled` hides it + temporarily; `archived`/`deleted` retire it without dropping historical bookings. +- `is_for_children` marks services booked on behalf of a child (via `child_id` on the booking). +- `requires_subscription` (default `true`) gates the service behind an active subscription. +- `form_id` optionally attaches an intake form (see `forms` / `form_questions`); it is + set to null if the form is deleted. +- **Bookings:** `child_id` is null for adult registrations. A partial unique index + (`service_bookings_service_id_child_id_idx`) prevents the same child from being + registered for the same service twice. `stripe_order_id` links the booking to its + Stripe payment. `is_active = false` hides a booking without deleting history.