Files
unsupervised-scheduler/docs/features/payments.md
T
thatguygriff 553cfafa49
CI / Tests (PHP 8.1) (pull_request) Successful in 51s
CI / Coding Standards (pull_request) Successful in 1m1s
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / No Debug Code (pull_request) Successful in 4s
CI / PHPStan (pull_request) Successful in 1m16s
CI / Tests (PHP 8.3) (pull_request) Successful in 45s
CI / Build Plugin Zip (pull_request) Has been skipped
Add HST/tax support and payment reporting with HST aggregation
Studio Settings gains a default HST rate; the rate is frozen onto each
payment at booking and computed against the pre-tax subtotal, with the
total billed as subtotal + tax. The rate is overridable per booking on
My Lessons while unpaid (recomputing the tax amount), comped
registrations are never taxed, and receipts break out subtotal/HST/total.

Builds the payments report (roadmap #8) from us_payments: a monthly
per-instructor view with subtotal, HST collected, and grand-total
aggregation, plus a nonce-protected CSV export via admin-post. Studio
admins see all instructors and can filter; instructors are scoped to
their own rows. The Payment Report menu is gated on export_payments so
instructors (who lack manage_billing) can reach it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:29:48 -03:00

7.8 KiB
Raw Blame History

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_id is 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 a card payment is created pending and 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
us_etransfer_email Studio-default e-transfer destination
us_hst_rate Default HST/tax percentage, e.g. 13

HST / Tax

A studio-default HST rate (percentage) is configured on Studio Settings (us_hst_rate, manage_billing). At booking the rate is frozen onto the payment (us_payments.tax_rate) and the tax is computed against the pre-tax subtotal (tax_amount = round(amount × rate / 100, 2)). The billed total is amount + tax_amount (Payment::total()). Comped registrations are never taxed (rate and amount are 0).

The rate is overridable per booking on My Lessons (instructor) while the payment is still unpaid; saving recomputes tax_amount from the current subtotal (PaymentRepository::updateTax). Receipts break out subtotal, HST, and total when tax applies.

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:

  1. Offering overrideus_offerings.etransfer_email, set by the instructor on the offering.
  2. Studio default — the us_etransfer_email option (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
tax_rate DECIMAL(5,2) HST rate % frozen at booking; editable until paid
tax_amount DECIMAL(10,2) Computed tax in dollars (amount × tax_rate / 100)
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

  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