Add live Stripe card charges (PaymentIntent + Elements + webhook) #28

Merged
thatguygriff merged 1 commits from feature/payments-stripe into main 2026-06-08 18:59:42 +00:00
Owner

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.
  • PaymentEndpointPOST /payments/intent (book_lesson) and public, signature-verified POST /payments/webhook.
  • PaymentRepositorysetStripeIntentId / 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

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)
thatguygriff added 1 commit 2026-06-08 18:52:30 +00:00
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
925a4b79ba
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 <noreply@anthropic.com>
thatguygriff merged commit 0a78f4b1ac into main 2026-06-08 18:59:42 +00:00
thatguygriff deleted branch feature/payments-stripe 2026-06-08 18:59:42 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#28