# Stripe Integration Runbook Operational reference for RootNode's payment-provider abstraction (`platform/api/src/ payment-provider.js`) — configuration, local testing against both the deterministic test provider and real Stripe test mode, webhook setup, and what's left before a real, live-money launch. See `docs/PHASE9_COMMERCE_AND_LAUNCH.md` for the design/architecture story and `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` for the website's own contract. ## The two providers | | `PAYMENT_PROVIDER=test` (default) | `PAYMENT_PROVIDER=stripe` | |---|---|---| | Network calls | None — fully in-process | Real calls to Stripe's API | | Needs credentials | No | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` | | Checkout URL | A fake URL pointing back at your own `success_url` | A real Stripe-hosted Checkout page | | Webhook signature | A static shared secret (`TEST_PAYMENT_WEBHOOK_SECRET`, header `X-Test-Payment-Signature`) | Real Stripe HMAC signature (header `Stripe-Signature`), verified via Stripe's own SDK | | Real money | Never | Real in live mode; fake (test cards) in Stripe's own test mode | | Used by | Every automated test/rehearsal in this repo | Nothing yet — pending real credentials | `commerce.js` and every route above it call the SAME methods regardless of which provider is active (`createCheckoutSession`, `verifyAndParseWebhook`) — switching providers is an environment variable, never a code change. ## Environment variables | Variable | Default | Purpose | |---|---|---| | `PAYMENT_PROVIDER` | `test` | `test` or `stripe`. | | `STRIPE_SECRET_KEY` | unset | Required for `stripe`. Server-side only — never exposed to any browser-served code. | | `STRIPE_WEBHOOK_SECRET` | unset | Required for `stripe`. The signing secret Stripe gives you for a specific webhook endpoint (dashboard or `stripe listen`, see below) — NOT your API secret key. | | `TEST_PAYMENT_WEBHOOK_SECRET` | `test-payment-secret-not-for-production` | Only used by the `test` provider. Change if you need to isolate parallel test runs; otherwise the default is fine. | | `REGISTRATION_RESERVATION_TIMEOUT_MINUTES` | `30` | How long a paid offering's seat stays reserved before the reaper releases it if checkout is never completed. | | `RESERVATION_REAPER_POLL_MS` | `60000` | How often the reservation reaper checks for expired, unpaid reservations. | None of these belong in `frontend/` or anywhere a browser can read them — the Stripe secret key and webhook secret are server-only, exactly like `INTEGRATION_SHARED_SECRET`/ `PROVISIONER_SHARED_SECRET` already are in this codebase. ## Local testing against the test provider (no Stripe account needed) This is what every e2e suite and rehearsal in this repo actually runs against. Leave `PAYMENT_PROVIDER` unset (defaults to `test`) and drive the full cycle directly: ```bash # 1. Create a checkout session (server-to-server, same as the website would) curl -s -X POST http://localhost:4200/api/integrations/registrations//checkout \ -H 'Content-Type: application/json' \ -H 'X-RootNode-Integration-Key: dev-integration-secret-not-for-production' \ -d '{"success_url":"https://example.com/s","cancel_url":"https://example.com/c"}' # 2. Look up the provider_reference the checkout just created docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform \ -c "SELECT provider_reference FROM payment_transactions WHERE registration_id = '' ORDER BY created_at DESC LIMIT 1;" # 3. Build and POST a fake "payment succeeded" webhook event (Node, using the same provider # module the real server uses — see platform/test/e2e-phase9.js for the full pattern) node -e " const { getPaymentProvider } = require('./platform/api/src/payment-provider'); const p = getPaymentProvider(); const event = p.buildTestEvent({ providerReference: '', eventType: 'checkout.session.completed' }); console.log(JSON.stringify(event)); " curl -s -X POST http://localhost:4200/api/webhooks/payment \ -H 'Content-Type: application/json' \ -H "X-Test-Payment-Signature: " \ -d '' ``` To simulate a **failed payment** or **abandoned checkout**, use `eventType: 'payment_intent.payment_failed'` or `'checkout.session.expired'` in step 3 instead — see `platform/test/e2e-phase9.js` for both, and for the reservation-reaper path (never send a webhook at all; wait past `REGISTRATION_RESERVATION_TIMEOUT_MINUTES`, or lower it for a faster local test). ## Local testing against real Stripe TEST mode (once credentials exist) Not yet exercised in this repo — no Stripe account/credentials have been provided as of Phase 9.1 either (see `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the exact search performed and what's needed). Once they exist, `platform/test/stripe-integration-test.js` (added in Phase 9.1) automates most of the steps below — set `RUN_STRIPE_INTEGRATION_TEST=true` plus `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`/`OWNER_EMAIL`/`OWNER_PASSWORD` and run it; it refuses to run against a live-mode key and makes zero Stripe network calls unless explicitly enabled. Manually, or to understand what that script does: 1. Create (or use an existing) Stripe account, switch to **Test mode** in the dashboard. 2. Copy the test-mode **Secret key** (`sk_test_...`) into `STRIPE_SECRET_KEY`. 3. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli) and run: ```bash stripe login stripe listen --forward-to localhost:4200/api/webhooks/payment ``` The CLI prints a webhook signing secret (`whsec_...`) — set that as `STRIPE_WEBHOOK_SECRET` for the duration of the local session (it's ephemeral per `stripe listen` run; a real deployment instead creates a permanent webhook endpoint in the Stripe dashboard and uses ITS signing secret). 4. Set `PAYMENT_PROVIDER=stripe` and restart the API. 5. Drive the checkout flow through steps 2-4 of `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` — you now get back a REAL `checkout_url`. Open it in a browser and pay with a Stripe test card (`4242 4242 4242 4242`, any future expiry, any CVC, any ZIP). 6. `stripe listen` forwards Stripe's real webhook event to your local server; watch the server log and `payment_webhook_events` table for `processed`. 7. To simulate a failure without a real failing card, use Stripe's documented test card numbers for declines (e.g. `4000 0000 0000 0002`), or `stripe trigger payment_intent.payment_failed`. This is the recommended path to real validation before going live — it exercises the actual Stripe SDK, actual signature verification, and actual Stripe event delivery, with zero real money at risk. ## Going live (real money) 1. Switch the Stripe dashboard to **Live mode**, generate a live **Secret key** (`sk_live_...`), and create a permanent webhook endpoint pointed at the production API's `/api/webhooks/payment` URL — copy ITS signing secret (`whsec_...`, different from test mode's). 2. Set `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) and `PAYMENT_PROVIDER=stripe` in production configuration only — never commit these to git, never put them in a `.env` file that gets version-controlled (matching this repo's existing secret-handling convention for `PROVISIONER_SHARED_SECRET`/`INTEGRATION_SHARED_SECRET`). 3. Set a real price on the live offering (`PATCH /api/admin/offerings/:id`) and confirm `GET /api/admin/offerings/:id/readiness` shows `payment_provider: ready`. 4. Run one real, small, genuinely-refundable transaction against a real card before opening registration publicly, and confirm the full pipeline (checkout → webhook → confirmation → enrollment → activation email) end to end, then refund it via the admin UI or the Stripe dashboard directly. 5. Monitor `rootnode_payment_webhooks_total`/`rootnode_payments_total` (Prometheus metrics, see `docs/PHASE9_COMMERCE_AND_LAUNCH.md`) after opening registration. **This phase does not perform steps 1-4 above** — no real Stripe credentials were provided, and inventing/obtaining them is a business decision reserved for Chase, not something this phase invents. Everything up to this runbook is built and tested against the test provider; live validation is the one explicitly pending item. ## Troubleshooting | Symptom | Likely cause | |---|---| | `503 PROVIDER_NOT_CONFIGURED` on checkout | `PAYMENT_PROVIDER=stripe` but `STRIPE_SECRET_KEY` unset, or the `stripe` npm package failed to load. | | `400 invalid_signature` on every real Stripe webhook | `STRIPE_WEBHOOK_SECRET` doesn't match the endpoint Stripe actually delivered to (test-mode secret used against a live endpoint, or vice versa — each `stripe listen`/dashboard endpoint has its OWN secret). | | Webhook received but nothing changes | Check `payment_webhook_events.result` for that event id — `duplicate` means it was already processed (expected on a retry); check server logs for an exception if `result` is missing entirely. | | Seat never releases after an abandoned checkout | Confirm the reservation reaper is running (`RESERVATION_REAPER_POLL_MS` log line on server startup) and that `REGISTRATION_RESERVATION_TIMEOUT_MINUTES` hasn't been set unreasonably high. |