New: PHASE9_1_STRIPE_TEST_VALIDATION.md — baseline verification, the exact Stripe-credential search performed (none found), the Stripe architecture review (dynamic price_data, unchanged), what was proven vs. blocked per the mission's own step list, and the live-mode readiness checklist. Updated: STRIPE_INTEGRATION_RUNBOOK.md and WINTER_BREAK_LAUNCH_CHECKLIST.md to point at the new opt-in test and the blocked status. PHASE9_COMMERCE_AND_LAUNCH.md gets a short addendum. No application code changed this phase — no genuine defect was exposed without real Stripe traffic, so no regression rerun was required.
140 lines
9.1 KiB
Markdown
140 lines
9.1 KiB
Markdown
# 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/<id>/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 = '<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: '<provider_reference from step 2>', 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: <event.signature from above>" \
|
|
-d '<event.body from above>'
|
|
```
|
|
|
|
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. |
|