9 Commits
Author SHA1 Message Date
thatguygriffandClaude Opus 5 c077a653fb Let a studio pick its default payment method and disconnect Stripe
CI / No Debug Code (pull_request) Successful in 5s
CI / Tests (PHP 8.2) (pull_request) Successful in 54s
CI / Tests (PHP 8.1) (pull_request) Successful in 56s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / PHPStan (pull_request) Successful in 3m48s
CI / Tests (PHP 8.3) (pull_request) Successful in 5m31s
CI / Build Plugin Zip (pull_request) Skipped
Stripe configuration was a one-way door. Keys could be entered but never
removed, and entering them moved every student onto card billing at once,
so there was no way to have Stripe live and satisfy yourself that card
payments worked before committing the studio to them.

Two settings-page changes open both directions:

Default payment method (`us_default_payment_method`) is now an explicit
choice between card and e-transfer for students with no per-student
override, rather than something inferred from whether keys exist. Card
remains the default, so a site that adds keys and changes nothing else
behaves as before. BillingMethodResolver still degrades a card default to
e-transfer while Stripe is unconfigured — there is nothing to charge a
card with — and `comp` is deliberately not offerable studio-wide, since
it would silently stop billing everybody; an unrecognised stored value
reads back as card. Holding the default on e-transfer with Stripe live is
the staged-rollout path: move individual students to card on their detail
page, watch real charges land, then flip the studio over.

Clear Stripe configuration deletes the publishable key, secret key and
webhook signing secret and returns the mode to test, so a re-configuration
later cannot inherit live. Currency, HST, e-transfer and registration
settings are untouched, as are recorded payments. The button only appears
when some Stripe value is stored, and reuses the page's existing nonce and
`manage_billing` check.

`composer test` (915), `composer lint` and `composer cs` all pass. Options
only — no schema change, so no version bump.

Closes #173

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WyktWmwNRgMYuwe5eBuPZm
2026-08-20 12:43:29 -03:00
thatguygriffandClaude Opus 4.8 169f7b6a13 Accept whole days only for the studio cancellation cutoff
CI / Tests (PHP 8.2) (pull_request) Successful in 38s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Coding Standards (pull_request) Successful in 3m2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
The Studio Settings cutoff field now takes an integer number of days (step 1,
coerced with Val::int) instead of allowing half-day fractions, and displays the
stored hours rounded to whole days.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-23 12:10:55 -03:00
thatguygriffandClaude Opus 4.8 8b90b8d78d Add cancellation cutoff limiting how close to a lesson a student can cancel
CI / Tests (PHP 8.1) (pull_request) Successful in 41s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
Students can no longer cancel their own lesson online once it starts within a
configured window; instructors and studio admins can always cancel.

- Studio default `us_cancellation_cutoff_hours` (stored/computed in hours,
  entered and displayed in days under Studio Settings → Cancellations).
- Optional per-offering override `cancellation_cutoff_hours` (entered in hours);
  blank inherits the studio default, 0 allows anytime cancellation.
- `Booking\CancellationPolicy` resolves the effective window and decides;
  `BookingEndpoint::cancel()` returns a 403 `cancellation_closed` when too late.
  The instructor status endpoint and studio-admin student actions bypass it.

Closes #93

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-23 11:57:18 -03:00
thatguygriffandClaude Opus 4.8 7370755951 Add open student registration with email confirmation and approval
CI / Tests (PHP 8.1) (pull_request) Successful in 1m18s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m18s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 3m20s
CI / Coding Standards (pull_request) Successful in 3m25s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m33s
CI / Build Plugin Zip (pull_request) Skipped
Students could previously join by invite only. Add an optional
self-approval mode, toggled from Studio Settings → Registration: anyone
may sign up on the existing [us_student_register] page, confirm their
email via a tokenised link, and then be approved by a studio admin
before the account is usable.

- Enabling the toggle mirrors WordPress's own membership settings
  (users_can_register + default_role = us_student) and snapshots their
  previous values so disabling restores them.
- WordPress's native registration form is blocked while open
  registration is on (login_init redirect + registration_errors
  fail-safe + register_url) so it cannot bypass signup policy acceptance.
- Pending accounts: unconfirmed email cannot log in; confirmed but
  unapproved can log in but the booking capability is withheld and the
  booking page shows an "awaiting approval" screen.
- Approve/reject from Students → Pending Students; reject hard-deletes
  the account so the email is freed to re-apply.
- Invite registration is unchanged; both modes coexist.

Account lifecycle lives in user meta (RegistrationStatus); no new tables.

Closes #63

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 10:50:21 -03:00
thatguygriffandClaude Opus 4.8 061d09e034 Harden booking, offering exposure, payments, and invites
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / Coding Standards (pull_request) Successful in 55s
CI / PHPStan (pull_request) Successful in 1m7s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m41s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Build Plugin Zip (pull_request) Has been skipped
Security fixes from a pen-test review (issues #31–#37):

- #31 Booking no longer trusts a client-supplied offering_id: a slot-tied
  offering is authoritative and any offering used must belong to the slot's
  instructor, closing a free/misrouted-payment bypass.
- #34 Availability slot creation rejects an offering the instructor does not
  own (AvailabilityEndpoint now takes OfferingRepository).
- #32 Offering/question/policy listing endpoints now require book_lesson
  instead of being public (no anonymous consumer exists); Offering::toArray
  also omits etransfer_email from listings as defense-in-depth.
- #33 Slots are claimed atomically (UPDATE ... WHERE is_booked = 0) before a
  lesson is inserted, preventing a double-booking race.
- #35 A single weekly booking is capped (MAX_WEEKLY_OCCURRENCES) and only
  creates lessons for slots it actually claimed.
- #36 Stripe secret/webhook keys are write-only in the settings UI and a blank
  submit keeps the stored value; secrets are never echoed back into HTML.
- #37 Pending invites expire after 14 days (Invite::isAcceptable), enforced at
  registration and surfaced on the admin invites list.

Adds BookingEndpointTest plus Invite/Offering/AvailabilityRepository coverage
and minimal WP_REST_Request/WP_REST_Response stubs. composer test (200),
lint, and cs all green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-09 17:08:22 -03:00
thatguygriffandClaude Opus 4.8 925a4b79ba Add live Stripe card charges (PaymentIntent + Elements + webhook)
CI / No Debug Code (pull_request) Successful in 40s
CI / Tests (PHP 8.2) (pull_request) Successful in 48s
CI / Coding Standards (pull_request) Successful in 1m0s
CI / PHPStan (pull_request) Successful in 1m13s
CI / Tests (PHP 8.1) (pull_request) Successful in 2m9s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m8s
CI / Build Plugin Zip (pull_request) Has been skipped
Completes the deferred half of payments: real credit-card processing on
top of the existing ledger/e-transfer/comp foundation.

- StripeGateway wraps stripe/stripe-php: creates idempotent PaymentIntents
  (amount in cents, registration ids in metadata) and verifies webhook
  signatures. Stripe calls sit behind protected seams for unit testing.
- PaymentService::createIntent resolves the client-side step for a new
  registration (card → client secret; e-transfer → display data; comp →
  none) with caller-ownership enforcement.
- PaymentService::handleWebhook finalises a payment exactly once on
  payment_intent.succeeded (mark paid → confirm → receipt) and marks it
  failed on payment_intent.payment_failed.
- PaymentEndpoint: POST /payments/intent (book_lesson) and public,
  signature-verified POST /payments/webhook.
- PaymentRepository: setStripeIntentId / findByStripeIntentId.
- StudioSettings: us_stripe_webhook_secret option, with the webhook URL
  and required events surfaced on the settings page.
- Front end: shared payment.js mounts Stripe Payment Elements and confirms
  the card (or shows e-transfer instructions); Stripe.js enqueued only when
  configured. Wired into booking and group-class flows.

Tests: new StripeGatewayTest; PaymentService card-intent + webhook cases;
repository coverage. composer test/lint/cs all green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-08 15:51:37 -03:00
thatguygriffandClaude Opus 4.8 553cfafa49 Add HST/tax support and payment reporting with HST aggregation
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
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 <[email protected]>
2026-06-08 11:29:48 -03:00
thatguygriffandClaude Opus 4.8 9873cb5e30 Add e-transfer destination email (studio default + offering/booking overrides)
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 46s
CI / Tests (PHP 8.1) (pull_request) Successful in 52s
CI / Tests (PHP 8.3) (pull_request) Successful in 52s
CI / Tests (PHP 8.2) (pull_request) Successful in 57s
CI / PHPStan (pull_request) Successful in 1m12s
CI / Build Plugin Zip (pull_request) Has been skipped
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 <[email protected]>
2026-06-08 10:47:06 -03:00
thatguygriffandClaude Opus 4.8 6c4097b385 Add payments foundation (e-transfer/comp, Stripe config, receipts)
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / Tests (PHP 8.3) (pull_request) Successful in 50s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 1m2s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m0s
CI / PHPStan (pull_request) Successful in 1m4s
CI / Build Plugin Zip (pull_request) Has been skipped
Implements the payments foundation for #7. Without Stripe credentials
everything works on e-transfer (pending payment confirmed by a studio
admin); when Stripe keys are configured the default flips to credit card.
Per-student override (card/etransfer/comp) is set on the student detail.

- Schema: us_payments (amount DECIMAL dollars, method, status, receipt,
  stripe intent id).
- src/Payment/: Payment VO, PaymentRepository, StudioSettings (Stripe
  options + isStripeConfigured + settings page), BillingMethodResolver
  (per-student override; default card if configured else etransfer),
  ReceiptMailer, PaymentService (create at registration, link payment_id,
  comp->paid+confirm, markPaid->confirm+receipt), PaymentController
  (e-transfer confirmation queue), PaymentEndpoint (PATCH /payments/{id}).
- Booking + enrolment create the payment from the offering price; comp
  auto-confirms the lesson; setPaymentId on both repositories.
- Admin: Studio Settings + Payments menus (manage_billing); per-student
  billing method on the student detail page.
- Docs: payments.md + README updated.

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 confirmed like an e-transfer.

Tests: tests/Unit/Payment/ (VO, repository, resolver, service, mailer).
composer test (147), cs, and PHPStan level 6 all pass.

Refs #7

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-08 10:24:01 -03:00