The e-transfer destination is resolved at booking time (offering override -> studio default) and frozen onto the payment, so each record keeps where the student was directed. It can then be corrected per booking. - StudioSettings: us_etransfer_email option + a Default e-transfer email field on the Studio Settings page. - Offering: etransfer_email column/field (instructor override) across VO, repo, REST endpoint, admin controller, and form. - Payment: etransfer_email column on the payment (frozen record) + PaymentRepository::updateEtransferEmail; PaymentService freezes it from the offering override or studio default at creation; booking/enrolment pass the offering override. - My Lessons: instructors edit the e-transfer email per pending lesson payment (ownership-checked). - Payments queue: studio admin can correct the email at confirmation (for when a student sends it to the wrong place). - Docs updated. Tests: Payment/Offering rows + PaymentService freezing. composer test (148), cs, and PHPStan level 6 all pass. Refs #7 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.8 KiB
Feature: Payments
Overview
Payment is taken at registration. When Stripe is not configured the platform falls back to e-transfer — a pending payment a studio admin marks received — so everything works without any credentials. When Stripe is configured the default rail becomes the credit card. The studio admin can override any student's method (card / e-transfer / comp). 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.
Implemented: the payment ledger, studio settings, method resolution (e-transfer default with no Stripe; card default when configured; per-student override), the e-transfer/comp flow with admin confirmation + receipts, and integration into booking/enrolment (a registration's
payment_idis linked; comp auto-confirms; e-transfer stays pending until confirmed). Deferred to a follow-up: the live Stripe card charge (PaymentIntent + Stripe.js Elements + webhook +stripe/stripe-php). Until then acardpayment is createdpendingand can be confirmed like an e-transfer.
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 (Students → student detail → Billing method). When unset, the studio
default applies — card if Stripe is configured, otherwise etransfer
(BillingMethodResolver):
| 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 |
E-transfer Destination Email
Where students send e-transfers is resolved and frozen onto the payment at
booking time (us_payments.etransfer_email), so each record keeps the destination
the student was given. Resolution at creation:
- Offering override —
us_offerings.etransfer_email, set by the instructor on the offering. - Studio default — the
us_etransfer_emailoption (Studio Settings,manage_billing).
After booking, the destination on a payment can be corrected per booking:
- My Lessons — the instructor edits the e-transfer email for a pending lesson payment.
- Payments queue — when marking an e-transfer received, the studio admin can update the email it was actually sent to before confirming.
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 |
DECIMAL(10,2) | Charged amount in dollars (matches the offering price) |
currency |
VARCHAR(3) | ISO 4217, e.g. CAD |
method |
VARCHAR(20) | card / etransfer / comp |
status |
VARCHAR(20) | pending / paid / failed / refunded |
etransfer_email |
VARCHAR(191) | Frozen e-transfer destination; editable until confirmed |
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
- During registration the front-end calls
POST /payments/intent, which creates a Stripe PaymentIntent for acardstudent and returns the client secret. (etransferreturns apendingpayment;compreturns none.) - The browser confirms the card payment with Stripe.
- Stripe calls
POST /payments/webhook; onpayment_intent.succeededthe payment is markedpaid,paid_atis stamped, and the linked lesson/enrolment isconfirmed. - On transition to
paid,ReceiptMailerassigns areceipt_number, emails the student a receipt, and stampsreceipt_sent_at. - For an e-transfer, the studio admin later calls
PATCH /payments/{id}to mark itpaid, 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.phptests/Unit/Payment/PaymentTest.phptests/Unit/Payment/StripeGatewayTest.phptests/Unit/Payment/ReceiptMailerTest.php