Completes the deferred second half of payment processing: live credit-card charging via Stripe, on top of the existing ledger / e-transfer / comp foundation.
What changed
StripeGateway (new) wraps stripe/stripe-php: creates idempotent PaymentIntents (amount in cents, registration ids in metadata) and verifies webhook signatures. The Stripe calls sit behind protected seams so they're unit-testable without the SDK's magic-property graph or the network.
PaymentService::createIntent — resolves the client-side payment step for a freshly created registration: card → Stripe client secret + publishable key; e-transfer → display data; comp/paid → nothing. Enforces caller ownership.
PaymentService::handleWebhook — on payment_intent.succeeded finalises the matching payment exactly once (mark paid → confirm lesson → email receipt, reusing finalizePaid); on payment_intent.payment_failed marks it failed. Returns false only on bad signatures.
PaymentEndpoint — POST /payments/intent (book_lesson) and public, signature-verified POST /payments/webhook.
PaymentRepository — setStripeIntentId / findByStripeIntentId for webhook reconciliation.
StudioSettings — new us_stripe_webhook_secret option; the settings page shows the exact webhook URL and the events to subscribe to.
Front end — shared payment.js mounts Stripe's Payment Element and confirms the card (redirect: 'if_required') or shows e-transfer instructions; Stripe.js is enqueued only when configured. Wired into both the booking and group-class flows.
Operational note
Card payments flip to paid only when Stripe calls the webhook, so the studio must register the endpoint in the Stripe Dashboard and paste its whsec_… signing secret into Studio Settings (URL + events are spelled out on the settings page).
Build / install
No CI change required: the build job's composer build regenerates vendor/ from composer.json, so the Stripe SDK is bundled automatically. Verified the built zip is self-contained — a single top-level unsupervised-schedular/ folder including vendor/stripe/stripe-php/ and a regenerated autoloader — so it installs directly in WordPress.
Checks
composer test (180 tests), composer lint (PHPStan L6), composer cs (PHPCS) all green. New StripeGatewayTest; PaymentService gains card-intent + webhook coverage; repository coverage added.
Completes the deferred second half of payment processing: live credit-card charging via Stripe, on top of the existing ledger / e-transfer / comp foundation.
## What changed
- **StripeGateway** (new) wraps `stripe/stripe-php`: creates idempotent PaymentIntents (amount in cents, registration ids in metadata) and verifies webhook signatures. The Stripe calls sit behind protected seams so they're unit-testable without the SDK's magic-property graph or the network.
- **PaymentService::createIntent** — resolves the client-side payment step for a freshly created registration: card → Stripe client secret + publishable key; e-transfer → display data; comp/paid → nothing. Enforces caller ownership.
- **PaymentService::handleWebhook** — on `payment_intent.succeeded` finalises the matching payment exactly once (mark paid → confirm lesson → email receipt, reusing `finalizePaid`); on `payment_intent.payment_failed` marks it `failed`. Returns false only on bad signatures.
- **PaymentEndpoint** — `POST /payments/intent` (`book_lesson`) and public, signature-verified `POST /payments/webhook`.
- **PaymentRepository** — `setStripeIntentId` / `findByStripeIntentId` for webhook reconciliation.
- **StudioSettings** — new `us_stripe_webhook_secret` option; the settings page shows the exact webhook URL and the events to subscribe to.
- **Front end** — shared `payment.js` mounts Stripe's Payment Element and confirms the card (`redirect: 'if_required'`) or shows e-transfer instructions; Stripe.js is enqueued only when configured. Wired into both the booking and group-class flows.
## Operational note
Card payments flip to **paid** only when Stripe calls the webhook, so the studio must register the endpoint in the Stripe Dashboard and paste its `whsec_…` signing secret into Studio Settings (URL + events are spelled out on the settings page).
## Build / install
No CI change required: the `build` job's `composer build` regenerates `vendor/` from `composer.json`, so the Stripe SDK is bundled automatically. Verified the built zip is self-contained — a single top-level `unsupervised-schedular/` folder including `vendor/stripe/stripe-php/` and a regenerated autoloader — so it installs directly in WordPress.
## Checks
`composer test` (180 tests), `composer lint` (PHPStan L6), `composer cs` (PHPCS) all green. New `StripeGatewayTest`; `PaymentService` gains card-intent + webhook coverage; repository coverage added.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Completes the deferred second half of payment processing: live credit-card charging via Stripe, on top of the existing ledger / e-transfer / comp foundation.
What changed
stripe/stripe-php: creates idempotent PaymentIntents (amount in cents, registration ids in metadata) and verifies webhook signatures. The Stripe calls sit behind protected seams so they're unit-testable without the SDK's magic-property graph or the network.payment_intent.succeededfinalises the matching payment exactly once (mark paid → confirm lesson → email receipt, reusingfinalizePaid); onpayment_intent.payment_failedmarks itfailed. Returns false only on bad signatures.POST /payments/intent(book_lesson) and public, signature-verifiedPOST /payments/webhook.setStripeIntentId/findByStripeIntentIdfor webhook reconciliation.us_stripe_webhook_secretoption; the settings page shows the exact webhook URL and the events to subscribe to.payment.jsmounts Stripe's Payment Element and confirms the card (redirect: 'if_required') or shows e-transfer instructions; Stripe.js is enqueued only when configured. Wired into both the booking and group-class flows.Operational note
Card payments flip to paid only when Stripe calls the webhook, so the studio must register the endpoint in the Stripe Dashboard and paste its
whsec_…signing secret into Studio Settings (URL + events are spelled out on the settings page).Build / install
No CI change required: the
buildjob'scomposer buildregeneratesvendor/fromcomposer.json, so the Stripe SDK is bundled automatically. Verified the built zip is self-contained — a single top-levelunsupervised-schedular/folder includingvendor/stripe/stripe-php/and a regenerated autoloader — so it installs directly in WordPress.Checks
composer test(180 tests),composer lint(PHPStan L6),composer cs(PHPCS) all green. NewStripeGatewayTest;PaymentServicegains card-intent + webhook coverage; repository coverage added.🤖 Generated with Claude Code