Skip payment step for unpriced bookings, confirm them immediately, show students their lessons
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m46s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 47s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m46s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 47s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Has been skipped
Booking a slot with no priced offering created the lesson but no payment,
yet the front end still called POST /payments/intent, which 400ed with
"Could not start payment for this registration" — the student saw an error
while the backend held a claimed slot and a lesson stuck at pending.
- POST /bookings and POST /enrollments now return a `payment` summary
({id, method, status}) or null when nothing is owed; the JS only runs
the payment step when a payment exists.
- Bookings with nothing owed are confirmed at creation — there is no
payment step that would ever confirm them later.
- The booking page now shows the student's upcoming lessons (GET /bookings,
now scoped to upcoming non-cancelled lessons with slot start/end times)
with a pending-payment/confirmed status badge.
Fixes #53
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -33,7 +33,10 @@ a class at capacity rejects further enrolments.
|
||||
| `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`).
|
||||
`accepted_policy_version_ids[]`, and payment data (see `payments.md`). The
|
||||
response includes `id`, `status`, and `payment` — a `{id, method, status}`
|
||||
summary, or `null` when the class is free (the front end then skips the
|
||||
payment step).
|
||||
|
||||
`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.
|
||||
|
||||
@@ -26,9 +26,10 @@ Students register for a private lesson by choosing an offering, picking a time (
|
||||
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.
|
||||
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. A booking with nothing owed (no offering, or a free offering) creates no payment and is `confirmed` immediately.
|
||||
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`.
|
||||
10. The booking page also shows the student their upcoming lessons (`GET /bookings`) with a per-lesson status badge (pending payment / confirmed).
|
||||
|
||||
## Weekly Reservations
|
||||
A weekly reservation creates one `series_id` shared across N lesson rows (one per
|
||||
@@ -45,9 +46,13 @@ offering).
|
||||
|
||||
`POST /bookings` body: `offering_id`, `slot_id`, `recurrence`, `answers[]`
|
||||
(`question_id` → value), `accepted_policy_version_ids[]`, and payment data
|
||||
(see `payments.md`).
|
||||
(see `payments.md`). The response includes `ids`, the resulting lesson
|
||||
`status`, and `payment` — a `{id, method, status}` summary, or `null` when
|
||||
nothing is owed (the front end then skips the payment step).
|
||||
|
||||
`GET /bookings` returns the caller's own lessons (student view) or upcoming lessons for the instructor if the caller has `manage_availability`.
|
||||
`GET /bookings` returns the caller's upcoming, non-cancelled lessons (their own
|
||||
for students; the instructor's for callers with `manage_availability`), each
|
||||
with the slot's `start_dt`/`end_dt`.
|
||||
|
||||
Group classes follow the same registration flow but enrol against an offering of
|
||||
kind `group_class`; see `group-classes.md`.
|
||||
@@ -68,11 +73,12 @@ kind `group_class`; see `group-classes.md`.
|
||||
- REST endpoint: `Unsupervised\Schedular\Booking\BookingEndpoint`
|
||||
- Frontend: `Unsupervised\Schedular\Booking\BookingPage`, `Unsupervised\Schedular\Auth\LoginPage`
|
||||
|
||||
> **Payment seam:** payment is deferred to the Payments feature (#7). For now a
|
||||
> booking is created with `status = pending` and `payment_id = null`; the
|
||||
> instructor confirms via `PATCH /bookings/{id}/status`. When payments land, the
|
||||
> pay→confirm + receipt step plugs into this seam. `GET /policies?scope=booking`
|
||||
> returns just the booking-gate policies the form must collect.
|
||||
> **Payment seam:** a priced booking is created with `status = pending` and its
|
||||
> payment linked via `payment_id`; the lesson is confirmed when the payment is
|
||||
> settled (see `payments.md`) or manually via `PATCH /bookings/{id}/status`.
|
||||
> Unpriced bookings skip the seam entirely and are confirmed at creation.
|
||||
> `GET /policies?scope=booking` returns just the booking-gate policies the form
|
||||
> must collect.
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Booking/BookingRepositoryTest.php`
|
||||
|
||||
@@ -94,7 +94,7 @@ After booking, the destination on a payment can be corrected per booking:
|
||||
| `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.)
|
||||
1. During registration the front-end calls `POST /payments/intent` — but only when the registration response carried a `payment` summary (unpriced registrations return `payment: null` and skip the payment step). The intent call 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`.
|
||||
|
||||
Reference in New Issue
Block a user