Add feature specs for booking platform requirements
CI / Coding Standards (push) Successful in 2m23s
CI / PHPStan (push) Successful in 59s
CI / Tests (PHP 8.1) (push) Successful in 50s
CI / Tests (PHP 8.2) (push) Successful in 51s
CI / Tests (PHP 8.3) (push) Successful in 48s
CI / No Debug Code (push) Successful in 3s
CI / Coding Standards (push) Successful in 2m23s
CI / PHPStan (push) Successful in 59s
CI / Tests (PHP 8.1) (push) Successful in 50s
CI / Tests (PHP 8.2) (push) Successful in 51s
CI / Tests (PHP 8.3) (push) Successful in 48s
CI / No Debug Code (push) Successful in 3s
Update availability, lesson-booking, and user-roles docs and add specs for offerings, group classes, registration questions, versioned policies, Stripe payments (with e-transfer/comp overrides and receipts), and monthly per-instructor payment reporting. Tracked in issues #1-#9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,57 @@
|
||||
# Feature: Availability Management
|
||||
|
||||
## Overview
|
||||
Instructors define date/time windows during which they are available for lessons. Students book from these windows.
|
||||
Instructors define date/time windows during which they are available for private lessons. Students book from these windows. Windows carry a lesson length and may be generated as a weekly-recurring series.
|
||||
|
||||
## Data Model — `{prefix}us_availability`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|----------------|------------------|----------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `instructor_id`| BIGINT UNSIGNED | WordPress user ID |
|
||||
| `start_dt` | DATETIME | Slot start — stored as `Y-m-d H:i:s` |
|
||||
| `end_dt` | DATETIME | Slot end — stored as `Y-m-d H:i:s` |
|
||||
| `is_booked` | TINYINT(1) | 0 = available, 1 = booked |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
| Column | Type | Notes |
|
||||
|--------------------|------------------|-------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `instructor_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `offering_id` | BIGINT UNSIGNED | Nullable FK → `us_offerings.id` (private-lesson type) |
|
||||
| `start_dt` | DATETIME | Slot start — stored as `Y-m-d H:i:s` |
|
||||
| `end_dt` | DATETIME | Slot end — stored as `Y-m-d H:i:s` |
|
||||
| `duration_minutes` | SMALLINT | Lesson length the window accommodates (e.g. 30, 60) |
|
||||
| `is_booked` | TINYINT(1) | 0 = available, 1 = booked |
|
||||
| `recurrence_group` | BIGINT UNSIGNED | Nullable — weekly-recurring windows share one group id |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
A window's `duration_minutes` is matched against the offering a student picks: a
|
||||
30-minute private offering can only be booked into a window whose
|
||||
`duration_minutes` accommodates it.
|
||||
|
||||
## Weekly-Recurring Windows
|
||||
Instructors may generate a window weekly across a date range. Each occurrence is a
|
||||
separate row sharing one `recurrence_group` id, so a recurring set can be added or
|
||||
removed together while individual occurrences are still booked independently.
|
||||
|
||||
## Admin Interface
|
||||
Instructors access **My Availability** in wp-admin (`?page=us-availability`).
|
||||
- Add a slot: provide start and end datetime
|
||||
- Add a slot: provide start/end datetime, duration, and (optionally) a linked private-lesson offering
|
||||
- Add a weekly series: provide the weekday/time plus a date range
|
||||
- Delete a slot: only allowed if `is_booked = 0`
|
||||
|
||||
## Public Calendar
|
||||
The front-end booking shortcode renders a month/week calendar of open windows,
|
||||
populated from `GET /availability`. Students can filter by instructor and by
|
||||
offering/duration before selecting a slot to register for.
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|-----------------------------------|-------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/availability` | `book_lesson` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/availability` | `manage_availability` |
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|-----------------------------------------------|------------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/availability` | `book_lesson` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/availability` | `manage_availability` |
|
||||
| `DELETE` | `/wp-json/us-scheduler/v1/availability/{id}` | `manage_availability` + slot owner |
|
||||
|
||||
`GET` supports query params: `instructor_id`, `from` (datetime), `to` (datetime).
|
||||
`GET` supports query params: `instructor_id`, `offering_id`, `duration_minutes`, `from` (datetime), `to` (datetime).
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\Data\AvailabilityRepository`
|
||||
- Model: `Unsupervised\Schedular\Model\AvailabilitySlot`
|
||||
- Admin controller: `Unsupervised\Schedular\Admin\AvailabilityController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Api\AvailabilityEndpoint`
|
||||
- Repository: `Unsupervised\Schedular\Availability\AvailabilityRepository`
|
||||
- Model: `Unsupervised\Schedular\Availability\AvailabilitySlot`
|
||||
- Admin controller: `Unsupervised\Schedular\Availability\AvailabilityController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Availability\AvailabilityEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Data/AvailabilityRepositoryTest.php`
|
||||
- `tests/Unit/Model/AvailabilitySlotTest.php`
|
||||
- `tests/Unit/Availability/AvailabilityRepositoryTest.php`
|
||||
- `tests/Unit/Availability/AvailabilitySlotTest.php`
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Feature: Group Classes
|
||||
|
||||
## Overview
|
||||
Students enrol in a group class — an offering of kind `group_class` — as a commitment for the year. Enrolment is capacity-enforced and billed full-term upfront. Registration reuses the same flow as private lessons (intake questions + policy acceptance + payment).
|
||||
|
||||
## Data Model — `{prefix}us_group_enrollments`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|----------------|------------------|-------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `offering_id` | BIGINT UNSIGNED | FK → `us_offerings.id` (kind = `group_class`) |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `instructor_id`| BIGINT UNSIGNED | WordPress user ID (denormalised from the offering) |
|
||||
| `status` | VARCHAR(20) | `active` / `cancelled` / `completed` |
|
||||
| `payment_id` | BIGINT UNSIGNED | Nullable FK → `us_payments.id` |
|
||||
| `enrolled_at` | DATETIME | Insertion time |
|
||||
|
||||
## Enrolment Flow
|
||||
1. Student opens a group class from the offering catalog.
|
||||
2. Student answers the offering's questions (`GET /offerings/{id}/questions`).
|
||||
3. Student accepts the current published policy versions (`GET /policies`) — required to continue.
|
||||
4. Full-term payment is taken per the student's billing method (card by default; `pending` for e-transfer; skipped for comp). See `payments.md`.
|
||||
5. `POST /enrollments` creates the enrolment (`status = active`), records answers and policy acceptances, and links the payment — but only if the offering's `capacity` has not been reached.
|
||||
6. On successful payment (or comp) a receipt is emailed.
|
||||
|
||||
Capacity is enforced at enrolment time by counting `active` rows for the offering;
|
||||
a class at capacity rejects further enrolments.
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|----------------------------------------------|----------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/enrollments` | Any logged-in user |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/enrollments` | `book_lesson` |
|
||||
|
||||
`POST /enrollments` body: `offering_id`, `answers[]` (`question_id` → value),
|
||||
`accepted_policy_version_ids[]`, and payment data (see `payments.md`).
|
||||
|
||||
`GET /enrollments` returns the caller's own enrolments, or all enrolments for the
|
||||
instructor's group classes if the caller has `view_own_lessons` on those offerings.
|
||||
|
||||
## Admin Interface
|
||||
- **Group Classes** (`manage_options` / studio admin): all enrolments across instructors
|
||||
- Instructors see enrolments for their own group classes under **My Lessons**
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\GroupClass\EnrollmentRepository`
|
||||
- Model: `Unsupervised\Schedular\GroupClass\Enrollment`
|
||||
- Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController`
|
||||
- REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/GroupClass/EnrollmentRepositoryTest.php`
|
||||
- `tests/Unit/GroupClass/EnrollmentTest.php`
|
||||
@@ -1,52 +1,72 @@
|
||||
# Feature: Lesson Booking
|
||||
|
||||
## Overview
|
||||
Students browse available slots and submit a booking. Instructors then confirm or cancel from wp-admin or via the REST API.
|
||||
Students register for a private lesson by choosing an offering, picking a time (or reserving a weekly slot for the term), answering the offering's intake questions, accepting current policies, and paying. Instructors confirm or cancel from wp-admin or via the REST API. A lesson becomes `confirmed` only once its payment is `paid` (or the student is comp'd).
|
||||
|
||||
## Data Model — `{prefix}us_lessons`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|----------------|------------------|--------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `slot_id` | BIGINT UNSIGNED | FK → `us_availability.id` |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `instructor_id`| BIGINT UNSIGNED | WordPress user ID (denormalised for fast queries) |
|
||||
| `status` | VARCHAR(20) | `pending` / `confirmed` / `cancelled` |
|
||||
| `notes` | TEXT | Optional student notes |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
| Column | Type | Notes |
|
||||
|----------------|------------------|-------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `slot_id` | BIGINT UNSIGNED | FK → `us_availability.id` |
|
||||
| `offering_id` | BIGINT UNSIGNED | FK → `us_offerings.id` (the private-lesson type booked) |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `instructor_id`| BIGINT UNSIGNED | WordPress user ID (denormalised for fast queries) |
|
||||
| `recurrence` | VARCHAR(10) | `single` or `weekly` |
|
||||
| `series_id` | BIGINT UNSIGNED | Nullable — groups the lesson rows of one weekly reservation |
|
||||
| `status` | VARCHAR(20) | `pending` / `confirmed` / `cancelled` |
|
||||
| `payment_id` | BIGINT UNSIGNED | Nullable FK → `us_payments.id` |
|
||||
| `notes` | TEXT | Optional student notes |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## Booking Flow
|
||||
1. Student opens the page with `[us_booking]` shortcode.
|
||||
2. JS fetches `GET /availability` → renders available slots.
|
||||
3. Student clicks **Book** → `POST /bookings` with `slot_id`.
|
||||
4. Server creates the lesson row (`status = pending`) and sets `us_availability.is_booked = 1`.
|
||||
5. Instructor sees the booking under **My Lessons** in wp-admin.
|
||||
6. Instructor updates status via `PATCH /bookings/{id}/status`.
|
||||
## Registration Flow
|
||||
1. Student opens the page with the `[us_booking]` shortcode and browses the calendar.
|
||||
2. Student picks an **offering** (a 30 or 60-minute private-lesson type) and a slot.
|
||||
3. For a `weekly` reservation, the same weekday/time is held for the rest of the offering's term.
|
||||
4. Student answers the offering's questions (`GET /offerings/{id}/questions`).
|
||||
5. Student accepts the current published policy versions (`GET /policies`) — required to continue.
|
||||
6. Payment is taken per the student's billing method (card by default; `pending` for e-transfer; skipped for comp). See `payments.md`.
|
||||
7. `POST /bookings` creates the lesson row(s) (`status = pending`), records answers and policy acceptances, marks `us_availability.is_booked = 1`, and links the payment.
|
||||
8. On successful payment (or comp) the lesson is `confirmed` and a receipt is emailed.
|
||||
9. Instructor sees the booking under **My Lessons** and may update status via `PATCH /bookings/{id}/status`.
|
||||
|
||||
## Weekly Reservations
|
||||
A weekly reservation creates one `series_id` shared across N lesson rows (one per
|
||||
week in the term) and reserves the matching availability windows. It is billed
|
||||
**full-term upfront** as a single payment (`billing_mode = full_term` on the
|
||||
offering).
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|-----------|------------------------------------------------|-------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/bookings` | Any logged-in user |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/bookings` | `book_lesson` |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/bookings/{id}/status`| `manage_availability` or admin |
|
||||
| Method | Endpoint | Permission |
|
||||
|-----------|-------------------------------------------------|--------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/bookings` | Any logged-in user |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/bookings` | `book_lesson` |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/bookings/{id}/status` | `manage_availability` or admin |
|
||||
|
||||
`POST /bookings` body: `offering_id`, `slot_id`, `recurrence`, `answers[]`
|
||||
(`question_id` → value), `accepted_policy_version_ids[]`, and payment data
|
||||
(see `payments.md`).
|
||||
|
||||
`GET /bookings` returns the caller's own lessons (student view) or upcoming lessons for the instructor if the caller has `manage_availability`.
|
||||
|
||||
Group classes follow the same registration flow but enrol against an offering of
|
||||
kind `group_class`; see `group-classes.md`.
|
||||
|
||||
## Admin Interface
|
||||
- **Scheduler** (`manage_options` only): all upcoming lessons across all instructors
|
||||
- **My Lessons** (`view_own_lessons`): upcoming lessons for the logged-in instructor
|
||||
|
||||
## Frontend Shortcodes
|
||||
- `[us_booking]` — student booking form; requires `book_lesson` capability
|
||||
- `[us_booking]` — student calendar + registration flow; requires `book_lesson` capability
|
||||
- `[us_student_login]` — front-end login form for students
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\Data\BookingRepository`
|
||||
- Model: `Unsupervised\Schedular\Model\Lesson`
|
||||
- Admin controller: `Unsupervised\Schedular\Admin\LessonController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Api\BookingEndpoint`
|
||||
- Frontend: `Unsupervised\Schedular\Frontend\BookingPage`, `LoginPage`
|
||||
- Repository: `Unsupervised\Schedular\Booking\BookingRepository`
|
||||
- Model: `Unsupervised\Schedular\Booking\Lesson`
|
||||
- Admin controller: `Unsupervised\Schedular\Booking\LessonController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Booking\BookingEndpoint`
|
||||
- Frontend: `Unsupervised\Schedular\Booking\BookingPage`, `Unsupervised\Schedular\Auth\LoginPage`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Data/BookingRepositoryTest.php`
|
||||
- `tests/Unit/Model/LessonTest.php`
|
||||
- `tests/Unit/Booking/BookingRepositoryTest.php`
|
||||
- `tests/Unit/Booking/LessonTest.php`
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Feature: Offerings
|
||||
|
||||
## Overview
|
||||
An offering is anything a student can register for: a private-lesson type (30 or 60 minutes, single or weekly) or a group class. Offerings carry pricing, billing mode, and the intake questions a registrant must answer. They are the catalog the booking calendar and group-class pages are built from.
|
||||
|
||||
## Data Model — `{prefix}us_offerings`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------------------|------------------|-------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `instructor_id` | BIGINT UNSIGNED | WordPress user ID of the owning instructor |
|
||||
| `kind` | VARCHAR(20) | `private_lesson` or `group_class` |
|
||||
| `title` | VARCHAR(191) | Display name |
|
||||
| `description` | TEXT | Optional longer description |
|
||||
| `duration_minutes` | SMALLINT | Private lessons only (e.g. 30, 60); NULL for group classes |
|
||||
| `price_cents` | INT UNSIGNED | Price in the smallest currency unit |
|
||||
| `currency` | VARCHAR(3) | ISO 4217, e.g. `CAD` |
|
||||
| `billing_mode` | VARCHAR(20) | `one_time` (single booking) or `full_term` (weekly / group) |
|
||||
| `allow_weekly` | TINYINT(1) | Private only — may be reserved weekly for the term |
|
||||
| `capacity` | SMALLINT | Group only — max enrolments; NULL for private |
|
||||
| `term_start` | DATE | Group / term offerings — first day; NULL otherwise |
|
||||
| `term_end` | DATE | Group / term offerings — last day; NULL otherwise |
|
||||
| `schedule_note` | VARCHAR(191) | Group only — human-readable schedule, e.g. "Tuesdays 4:00pm"|
|
||||
| `is_active` | TINYINT(1) | 0 = hidden from registration, 1 = bookable |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## Billing Mode
|
||||
- `one_time` — charged once at booking (a single private lesson).
|
||||
- `full_term` — charged in full upfront at registration (a weekly private reservation or a year-long group class). See `payments.md`.
|
||||
|
||||
## Admin Interface
|
||||
Studio admin and instructors manage offerings under **Offerings** in wp-admin.
|
||||
- Studio admin (`manage_offerings`) manages offerings for any instructor.
|
||||
- Instructor (`manage_offerings`) manages only their own.
|
||||
- Each offering's intake questions are edited from the offering screen (see `registration-questions.md`).
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|---------------------------------------------|----------------------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/offerings` | Public (active offerings only) |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/offerings` | `manage_offerings` |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/offerings/{id}` | `manage_offerings` + owner |
|
||||
| `DELETE` | `/wp-json/us-scheduler/v1/offerings/{id}` | `manage_offerings` + owner |
|
||||
|
||||
`GET` supports query params: `instructor_id`, `kind`.
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\Offering\OfferingRepository`
|
||||
- Model: `Unsupervised\Schedular\Offering\Offering`
|
||||
- Admin controller: `Unsupervised\Schedular\Offering\OfferingController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Offering\OfferingEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Offering/OfferingRepositoryTest.php`
|
||||
- `tests/Unit/Offering/OfferingTest.php`
|
||||
@@ -0,0 +1,49 @@
|
||||
# Feature: Payment Reporting
|
||||
|
||||
## Overview
|
||||
A monthly view of payments per instructor, with a downloadable spreadsheet (CSV) export. The studio admin sees every instructor and can filter; each instructor sees only their own payments. The report is built entirely from `us_payments` — no additional table.
|
||||
|
||||
## Data Source
|
||||
Reads `{prefix}us_payments` (see `payments.md`), grouped by calendar month and
|
||||
instructor. Each report row joins through to the student and offering for display:
|
||||
|
||||
| Report Column | Source |
|
||||
|---------------|---------------------------------------------------|
|
||||
| Date | `us_payments.paid_at` (falls back to `created_at`) |
|
||||
| Student | `us_payments.student_id` → display name |
|
||||
| Offering | registration → `us_offerings.title` |
|
||||
| Method | `us_payments.method` |
|
||||
| Status | `us_payments.status` |
|
||||
| Amount | `us_payments.amount_cents` / `currency` |
|
||||
|
||||
Totals are summed over `paid` rows for the selected month.
|
||||
|
||||
## Access Rules
|
||||
- Studio admin (`view_all_payments`): all instructors; may filter by `instructor_id`.
|
||||
- Instructor (`view_own_payments`): rows where `us_payments.instructor_id` is their own user ID only.
|
||||
- Export requires `export_payments` and is scoped to the same rows the caller may view.
|
||||
|
||||
## Admin Interface
|
||||
**Payments** in wp-admin:
|
||||
- Month picker (defaults to the current month) and, for studio admin, an instructor filter
|
||||
- A table of payments with a monthly total
|
||||
- An **Export** button that downloads the filtered rows as CSV
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|--------|------------------------------------------------|----------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/payments` | `view_own_payments` or `view_all_payments` |
|
||||
| `GET` | `/wp-json/us-scheduler/v1/payments/export` | `export_payments` |
|
||||
|
||||
Query params: `month` (`YYYY-MM`, required), `instructor_id` (optional; ignored for
|
||||
instructors, who are always scoped to themselves). `GET /payments/export` returns
|
||||
`text/csv` with a `Content-Disposition` attachment header.
|
||||
|
||||
## Implementation
|
||||
- Report controller: `Unsupervised\Schedular\Payment\PaymentReportController`
|
||||
- CSV exporter: `Unsupervised\Schedular\Payment\PaymentExporter`
|
||||
- Report query: `Unsupervised\Schedular\Payment\PaymentRepository::report()`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Payment/PaymentReportControllerTest.php`
|
||||
- `tests/Unit/Payment/PaymentExporterTest.php`
|
||||
@@ -0,0 +1,74 @@
|
||||
# Feature: Payments
|
||||
|
||||
## Overview
|
||||
Payment is taken at registration. The default rail is a credit card charged through Stripe, but the studio admin can set any student to pay by e-transfer (recorded, marked paid manually) or to be comped (no charge). Single bookings are charged once; weekly reservations and group classes are charged the full term upfront. A numbered receipt is emailed automatically when a payment is marked paid.
|
||||
|
||||
## Stripe Configuration
|
||||
Stripe credentials live in WordPress options, managed on the **Studio Settings**
|
||||
page (`manage_billing`, studio admin only):
|
||||
|
||||
| Option | Notes |
|
||||
|------------------------------|----------------------------------------|
|
||||
| `us_stripe_publishable_key` | Stripe publishable key |
|
||||
| `us_stripe_secret_key` | Stripe secret key |
|
||||
| `us_stripe_mode` | `test` or `live` |
|
||||
| `us_currency` | Default ISO 4217 currency, e.g. `CAD` |
|
||||
|
||||
## Per-Student Billing Method
|
||||
Each student's billing method is stored in user meta `us_payment_method`, set by the
|
||||
studio admin (default `card`):
|
||||
|
||||
| Method | Behaviour |
|
||||
|------------|-----------------------------------------------------------------------|
|
||||
| `card` | Charged immediately via Stripe; payment `paid` on success |
|
||||
| `etransfer`| Payment row created `pending`; admin marks it `paid` when funds arrive |
|
||||
| `comp` | No charge; registration is confirmed immediately, no payment row required |
|
||||
|
||||
## Data Model — `{prefix}us_payments`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|----------------------------|------------------|--------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `instructor_id` | BIGINT UNSIGNED | WordPress user ID (denormalised for reporting) |
|
||||
| `registration_type` | VARCHAR(20) | `lesson` or `enrollment` |
|
||||
| `registration_id` | BIGINT UNSIGNED | FK → `us_lessons.id` or `us_group_enrollments.id` |
|
||||
| `amount_cents` | INT UNSIGNED | Charged amount in the smallest currency unit |
|
||||
| `currency` | VARCHAR(3) | ISO 4217, e.g. `CAD` |
|
||||
| `method` | VARCHAR(20) | `card` / `etransfer` / `comp` |
|
||||
| `status` | VARCHAR(20) | `pending` / `paid` / `failed` / `refunded` |
|
||||
| `stripe_payment_intent_id` | VARCHAR(255) | Stripe PaymentIntent id; NULL for e-transfer / comp |
|
||||
| `receipt_number` | VARCHAR(50) | Sequential receipt id; set when `paid` |
|
||||
| `receipt_sent_at` | DATETIME | When the receipt email was sent; NULL until sent |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
| `paid_at` | DATETIME | When marked `paid`; NULL otherwise |
|
||||
|
||||
## Payment Flow
|
||||
1. During registration the front-end calls `POST /payments/intent`, which creates a Stripe PaymentIntent for a `card` student and returns the client secret. (`etransfer` returns a `pending` payment; `comp` returns none.)
|
||||
2. The browser confirms the card payment with Stripe.
|
||||
3. Stripe calls `POST /payments/webhook`; on `payment_intent.succeeded` the payment is marked `paid`, `paid_at` is stamped, and the linked lesson/enrolment is `confirmed`.
|
||||
4. On transition to `paid`, `ReceiptMailer` assigns a `receipt_number`, emails the student a receipt, and stamps `receipt_sent_at`.
|
||||
5. For an e-transfer, the studio admin later calls `PATCH /payments/{id}` to mark it `paid`, which triggers the same confirmation + receipt.
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|---------|---------------------------------------------|-----------------------------|
|
||||
| `POST` | `/wp-json/us-scheduler/v1/payments/intent` | `book_lesson` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/payments/webhook` | Public (Stripe signature verified) |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/payments/{id}` | `manage_billing` |
|
||||
|
||||
See `payment-reporting.md` for the monthly report and CSV export endpoints.
|
||||
|
||||
## Implementation
|
||||
- Repository: `Unsupervised\Schedular\Payment\PaymentRepository`
|
||||
- Model: `Unsupervised\Schedular\Payment\Payment`
|
||||
- Stripe gateway: `Unsupervised\Schedular\Payment\StripeGateway`
|
||||
- Receipts: `Unsupervised\Schedular\Payment\ReceiptMailer`
|
||||
- Settings page: `Unsupervised\Schedular\Payment\StudioSettings`
|
||||
- REST endpoint: `Unsupervised\Schedular\Payment\PaymentEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Payment/PaymentRepositoryTest.php`
|
||||
- `tests/Unit/Payment/PaymentTest.php`
|
||||
- `tests/Unit/Payment/StripeGatewayTest.php`
|
||||
- `tests/Unit/Payment/ReceiptMailerTest.php`
|
||||
@@ -0,0 +1,72 @@
|
||||
# Feature: Policies
|
||||
|
||||
## Overview
|
||||
The studio admin drafts, versions, and publishes policies (e.g. cancellation, payment, code of conduct). Registrants must read and accept the current published version of every policy before they can book. Acceptance is recorded against the specific version, so when a policy is updated students must re-accept it at their next booking.
|
||||
|
||||
## Data Model — `{prefix}us_policies`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|----------------------|------------------|------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `title` | VARCHAR(191) | Display name |
|
||||
| `slug` | VARCHAR(191) | Unique key, e.g. `cancellation` |
|
||||
| `current_version_id` | BIGINT UNSIGNED | Nullable FK → `us_policy_versions.id` (published) |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## Data Model — `{prefix}us_policy_versions`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|------------------|------------------|----------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `policy_id` | BIGINT UNSIGNED | FK → `us_policies.id` |
|
||||
| `version_number` | INT | Increments per policy, starting at 1 |
|
||||
| `body` | LONGTEXT | The policy text (HTML/markdown) |
|
||||
| `status` | VARCHAR(20) | `draft` / `published` / `archived` |
|
||||
| `published_at` | DATETIME | When published; NULL for drafts |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## Data Model — `{prefix}us_policy_acceptances`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---------------------|------------------|--------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `policy_version_id` | BIGINT UNSIGNED | FK → `us_policy_versions.id` (the exact version accepted) |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID |
|
||||
| `registration_type` | VARCHAR(20) | `lesson` or `enrollment` |
|
||||
| `registration_id` | BIGINT UNSIGNED | FK → `us_lessons.id` or `us_group_enrollments.id` |
|
||||
| `accepted_at` | DATETIME | Timestamp of acceptance |
|
||||
| `ip_address` | VARCHAR(45) | IP captured at acceptance (audit trail) |
|
||||
|
||||
## Versioning & Acceptance Rules
|
||||
- Editing a published policy creates a new `draft` version; the old version stays `published` until the draft is published.
|
||||
- Publishing a draft sets it `published`, stamps `published_at`, archives the prior version, and points `us_policies.current_version_id` at it.
|
||||
- The registration gate requires acceptance of the `current_version_id` of every policy. Because acceptance is tied to `policy_version_id`, a newly published version is unaccepted and must be re-accepted at the student's next booking.
|
||||
|
||||
## Admin Interface
|
||||
**Policies** in wp-admin (`manage_policies`, studio admin only):
|
||||
- Create a policy; draft and edit version bodies
|
||||
- Publish a draft version; view acceptance history per version
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|-----------------------------------------------------------------|-------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/policies` | Public (current published versions) |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/policies` | `manage_policies` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/policies/{id}/versions` | `manage_policies` |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/policies/{id}/versions/{vid}` | `manage_policies` |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/policies/{id}/versions/{vid}/publish` | `manage_policies` |
|
||||
|
||||
Acceptances are not posted directly — they are written as part of `POST /bookings`
|
||||
and `POST /enrollments` via the `accepted_policy_version_ids[]` field, which must
|
||||
cover every policy's current version or the registration is rejected.
|
||||
|
||||
## Implementation
|
||||
- Repositories: `Unsupervised\Schedular\Policy\PolicyRepository`, `Unsupervised\Schedular\Policy\PolicyVersionRepository`, `Unsupervised\Schedular\Policy\AcceptanceRepository`
|
||||
- Models: `Unsupervised\Schedular\Policy\Policy`, `Unsupervised\Schedular\Policy\PolicyVersion`, `Unsupervised\Schedular\Policy\PolicyAcceptance`
|
||||
- Admin controller: `Unsupervised\Schedular\Policy\PolicyController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Policy\PolicyEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Policy/PolicyRepositoryTest.php`
|
||||
- `tests/Unit/Policy/PolicyVersionRepositoryTest.php`
|
||||
- `tests/Unit/Policy/AcceptanceRepositoryTest.php`
|
||||
@@ -0,0 +1,63 @@
|
||||
# Feature: Registration Questions
|
||||
|
||||
## Overview
|
||||
Each offering can carry a set of intake questions the registrant must answer when booking. Questions are authored per offering by the studio admin or the owning instructor, and answers are stored against the resulting lesson or group enrolment.
|
||||
|
||||
## Data Model — `{prefix}us_questions`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---------------|------------------|-------------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `offering_id` | BIGINT UNSIGNED | FK → `us_offerings.id` — questions are scoped per offering |
|
||||
| `label` | VARCHAR(255) | The question text shown to the registrant |
|
||||
| `field_type` | VARCHAR(20) | `text` / `textarea` / `select` / `checkbox` |
|
||||
| `options` | TEXT | JSON array of choices (for `select`); NULL otherwise |
|
||||
| `is_required` | TINYINT(1) | 1 = registrant must answer to continue |
|
||||
| `sort_order` | INT | Display order within the offering |
|
||||
| `is_active` | TINYINT(1) | 0 = retired, 1 = shown on the form |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
## Data Model — `{prefix}us_question_answers`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---------------------|------------------|--------------------------------------------------------|
|
||||
| `id` | BIGINT UNSIGNED | Primary key |
|
||||
| `question_id` | BIGINT UNSIGNED | FK → `us_questions.id` |
|
||||
| `registration_type` | VARCHAR(20) | `lesson` or `enrollment` |
|
||||
| `registration_id` | BIGINT UNSIGNED | FK → `us_lessons.id` or `us_group_enrollments.id` |
|
||||
| `student_id` | BIGINT UNSIGNED | WordPress user ID (denormalised for fast lookup) |
|
||||
| `answer_value` | TEXT | The submitted answer (checkbox stored as `0`/`1`) |
|
||||
| `created_at` | DATETIME | Insertion time |
|
||||
|
||||
The `registration_type` + `registration_id` pair is a polymorphic reference shared
|
||||
with `us_policy_acceptances` (see `policies.md`), letting answers attach to either a
|
||||
private lesson or a group enrolment.
|
||||
|
||||
## Flow
|
||||
1. On the registration form, the front-end calls `GET /offerings/{id}/questions`.
|
||||
2. Required questions block submission until answered.
|
||||
3. Answers are sent in the `answers[]` array on `POST /bookings` or `POST /enrollments` and written to `us_question_answers` alongside the new registration row.
|
||||
|
||||
## Admin Interface
|
||||
Questions are edited from each offering's screen (**Offerings → Questions**).
|
||||
- Studio admin (`manage_questions`) edits questions on any offering.
|
||||
- Instructor (`manage_questions`) edits questions only on their own offerings.
|
||||
|
||||
## REST API
|
||||
| Method | Endpoint | Permission |
|
||||
|----------|---------------------------------------------------|----------------------|
|
||||
| `GET` | `/wp-json/us-scheduler/v1/offerings/{id}/questions`| Public |
|
||||
| `POST` | `/wp-json/us-scheduler/v1/questions` | `manage_questions` |
|
||||
| `PATCH` | `/wp-json/us-scheduler/v1/questions/{id}` | `manage_questions` + owner |
|
||||
| `DELETE` | `/wp-json/us-scheduler/v1/questions/{id}` | `manage_questions` + owner |
|
||||
|
||||
## Implementation
|
||||
- Repositories: `Unsupervised\Schedular\Registration\QuestionRepository`, `Unsupervised\Schedular\Registration\AnswerRepository`
|
||||
- Models: `Unsupervised\Schedular\Registration\Question`, `Unsupervised\Schedular\Registration\Answer`
|
||||
- Admin controller: `Unsupervised\Schedular\Registration\QuestionController`
|
||||
- REST endpoint: `Unsupervised\Schedular\Registration\QuestionEndpoint`
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Registration/QuestionRepositoryTest.php`
|
||||
- `tests/Unit/Registration/AnswerRepositoryTest.php`
|
||||
- `tests/Unit/Registration/QuestionTest.php`
|
||||
+46
-10
@@ -1,28 +1,64 @@
|
||||
# Feature: User Roles
|
||||
|
||||
## Overview
|
||||
Two custom WordPress user roles control access to all scheduling features.
|
||||
Three custom WordPress user roles control access to all scheduling features: a studio admin who runs the business, instructors who teach, and students who book.
|
||||
|
||||
## Roles
|
||||
|
||||
### Instructor (`us_instructor`)
|
||||
Created on plugin activation. Logs in via standard wp-admin. Can:
|
||||
- Manage their own availability slots (add/delete)
|
||||
- View their upcoming confirmed/pending lessons in wp-admin
|
||||
### Studio Admin (`us_studio_admin`)
|
||||
Runs the studio. Logs in via standard wp-admin. Can:
|
||||
- Create instructor accounts and set/revoke each instructor's capabilities
|
||||
- Manage offerings, intake questions, and policies
|
||||
- Configure Stripe credentials and per-student billing overrides (card / e-transfer / comp)
|
||||
- View the all-instructor payments report and export it
|
||||
|
||||
**Capabilities:** `read`, `manage_availability`, `view_own_lessons`
|
||||
**Capabilities:** `read`, `manage_instructors`, `manage_offerings`, `manage_questions`, `manage_policies`, `manage_billing`, `view_all_payments`, `export_payments`
|
||||
|
||||
### Instructor (`us_instructor`)
|
||||
Created by the studio admin. Logs in via standard wp-admin. Can:
|
||||
- Manage their own availability slots (add/delete), including weekly-recurring windows
|
||||
- Manage their own offerings and intake questions
|
||||
- View their upcoming confirmed/pending lessons and group enrolments
|
||||
- View and export their own payments
|
||||
|
||||
**Capabilities:** `read`, `manage_availability`, `manage_offerings`, `manage_questions`, `view_own_lessons`, `view_own_payments`, `export_payments`
|
||||
|
||||
### Student (`us_student`)
|
||||
Logs in via the front-end `[us_student_login]` shortcode. Can:
|
||||
- Browse available lesson slots from all instructors
|
||||
- Book a lesson slot
|
||||
- Browse available lesson slots and offerings from all instructors
|
||||
- Book a private lesson (single or weekly) and enrol in group classes
|
||||
|
||||
**Capabilities:** `read`, `book_lesson`, `view_own_lessons`
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
| Capability | Studio Admin | Instructor | Student | Used by |
|
||||
|-----------------------|:------------:|:----------:|:-------:|---------------------------------|
|
||||
| `manage_instructors` | ✓ | | | Instructor management |
|
||||
| `manage_availability` | | ✓ | | Availability |
|
||||
| `manage_offerings` | ✓ | ✓ (own) | | Offerings |
|
||||
| `manage_questions` | ✓ | ✓ (own) | | Registration questions |
|
||||
| `manage_policies` | ✓ | | | Policies |
|
||||
| `manage_billing` | ✓ | | | Payments (Stripe + overrides) |
|
||||
| `book_lesson` | | | ✓ | Lesson booking / enrolment |
|
||||
| `view_own_lessons` | | ✓ | ✓ | Lesson + group views |
|
||||
| `view_own_payments` | | ✓ | | Payment reporting |
|
||||
| `view_all_payments` | ✓ | | | Payment reporting |
|
||||
| `export_payments` | ✓ | ✓ (own) | | Payment reporting export |
|
||||
|
||||
## Instructor Management
|
||||
The studio admin gets an **Instructors** admin page (gated by `manage_instructors`)
|
||||
to add an instructor — creating the WP user with the `us_instructor` role — and to
|
||||
toggle that instructor's per-capability access (e.g. whether they may manage their
|
||||
own offerings/questions or export payments). The studio admin cannot grant a
|
||||
capability it does not itself hold.
|
||||
|
||||
## Implementation
|
||||
- Class: `Unsupervised\Schedular\Roles\RoleManager`
|
||||
- Class: `Unsupervised\Schedular\Auth\RoleManager`
|
||||
- Instructor management controller: `Unsupervised\Schedular\Auth\InstructorController`
|
||||
- Roles are created on `plugins_loaded → init` and on plugin activation via `Installer`.
|
||||
- Permissions are checked with `current_user_can()` against the capability string, not the role name.
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Roles/RoleManagerTest.php`
|
||||
- `tests/Unit/Auth/RoleManagerTest.php`
|
||||
- `tests/Unit/Auth/InstructorControllerTest.php`
|
||||
|
||||
Reference in New Issue
Block a user