Stripe Compliance Pack purchase flow · closes #3#8
Open
Frex22 wants to merge 1 commit into
Open
Conversation
Wires the demo monetization beat: products + PaymentIntent + signature- verified webhook + Stripe Elements modal + the Runbook F5 simulate-success fallback. Reuses the SSE bus, error envelope, ClickHouse Cloud, and shared schemas scaffolded in #2. - GET /v1/billing/products — single Compliance Pack at $999 one-time, with the org's current entitlement. - POST /v1/billing/payment-intents — creates a real Stripe PaymentIntent in test mode, returns clientSecret + publishableKey. Verified live against acct_1TaIsMJXawLng5dj (pi_3TaJSxJXawLng5dj03B8WmHX returned by the live smoke). - POST /webhooks/stripe — raw-body signature verification via stripe.webhooks.constructEvent. payment_intent.succeeded flips org.compliancePack, persists Action(kind:"payment", status:"delivered"), and emits org.entitlements.changed SSE. payment_intent.payment_failed persists a failed Action without changing entitlements. Unknown event types ack 200 no-op (handoff/API §07). - POST /v1/billing/simulate-success — dev-only (404 in production) fallback the modal calls on Shift+Enter when Elements fails on stage. Same observable side effects as a real webhook. - apps/web Stripe modal — @stripe/react-stripe-js + Elements + PaymentElement. States: loading-product → ready-to-pay → creating-intent → elements-ready → processing → success (driven by SSE org.entitlements.changed) → failure with retry. Already-entitled orgs short-circuit straight to success. - apps/api/src/providers/stripe.ts — singleton SDK + idempotent boot-time ensureStripeProducts() that retrieves-or-creates the compliance-pack product and price. - apps/api/src/db/actions.ts — ActionStore interface + ClickHouseActionStore writing the existing actions table; in-memory test double in tests/helpers. - apps/api/src/lib/entitlements.ts — Org cache mutation + bus publish. - packages/shared — EntitlementsChangedEvent added to the SseEvent union. Smoke verified end-to-end against live Stripe (test mode) and live ClickHouse Cloud: - POST products → 200 with the locked SKU + current entitlement - POST payment-intents → real Stripe pi_3TaJSxJXawLng5dj03B8WmHX - POST simulate-success → entitlement flips, Action row persisted to CH with payload.simulated:true Tests: 30/30 passing across 6 suites. Webhook tests use the real Stripe SDK to sign synthetic events (no network), so signature handling is exercised against the actual constructEvent path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stripe Compliance Pack slice — products + PaymentIntent + signature-verified webhook + Elements modal + Runbook F5 simulate-success fallback. Reuses the SSE bus, error envelope, ClickHouse Cloud store, and shared schemas scaffolded in #2.
Server
GET /v1/billing/products— single Compliance Pack at $999 one-time + current org entitlement.POST /v1/billing/payment-intents— creates a real Stripe PaymentIntent (test mode), returnsclientSecret+publishableKey. Verified live:pi_3TaJSxJXawLng5dj03B8WmHXis queryable via the Stripe API onacct_1TaIsMJXawLng5dj.POST /webhooks/stripe— raw-body signature verification viastripe.webhooks.constructEvent.payment_intent.succeeded→ flipsorg.compliancePack, persistsAction(kind:"payment", status:"delivered"), emitsorg.entitlements.changedSSEpayment_intent.payment_failed→ persistsAction(status:"failed"), no entitlement changePOST /v1/billing/simulate-success— dev-only (returns 404 in production). Runbook F5 fallback the modal calls on Shift+Enter when Elements fails on stage. Same observable side effects as a real webhook.apps/api/src/providers/stripe.ts— singleton SDK + idempotentensureStripeProducts()boot bootstrap.apps/api/src/db/actions.ts—ActionStoreinterface +ClickHouseActionStorewriting the existingactionstable (DDL from [Sprint 1 | Frontend/API | Frex22] Add Vendor flow with first-scan subscription #2).apps/api/src/lib/entitlements.ts— Org cache mutation + bus publish fororg.entitlements.changed.Web
apps/web/src/screens/StripeModal.tsx—@stripe/react-stripe-jswithElements+PaymentElement. States:loading-product→ready-to-pay→creating-intent→elements-ready→processing→success(driven by SSEorg.entitlements.changed) →failurewith retry. Already-entitled orgs short-circuit straight to success.apps/web/src/lib/api.ts— extends withgetBillingProducts(),createPaymentIntent(),simulateSuccess()(no rewrite of [Sprint 1 | Frontend/API | Frex22] Add Vendor flow with first-scan subscription #2 surface).apps/web/src/lib/stream.ts—useEntitlementshook subscribing to the SSE channel for org entitlement changes.apps/web/src/App.tsx— adds an Upgrade button in the header that opens the modal.Shared
packages/shared/src/types.ts— addsEntitlementsChangedEventto theSseEventunion.Caveats reviewers should know
stripe listen→/webhooks/stripe) not exercised live yet — needs the Stripe CLI authed before the demo. Signature handling is exhaustively covered in tests using the real Stripe SDK'sgenerateTestHeaderStringagainstconstructEvent(no network).STRIPE_WEBHOOK_SECRETis currently a dev placeholder in.env.local. Replace with the realwhsec_...printed bystripe listen --forward-to localhost:8787/webhooks/stripebefore demo./v1/billing/simulate-successis dev-only — returns 404 whenNODE_ENV=production. This is the explicit Runbook F5 fallback; the modal'sShift+Entercalls it.compliance-packproduct is created idempotently in Stripe at boot via the Management API (retrieve-or-create pattern).Smoke verified end-to-end
pnpm db:migrate— no-op ([Sprint 1 | Frontend/API | Frex22] Add Vendor flow with first-scan subscription #2 already applied the DDL);actionstable reused.GET /v1/billing/products→ 200 withcompliancePack:falseinitially.POST /v1/billing/payment-intents { sku:"compliance-pack" }→ 200 with realpi_3TaJSxJXawLng5dj03B8WmHX(verified via Stripe API).POST /v1/billing/simulate-success→ 200,compliancePackflips totrue,actionsrow inserted in ClickHouse (kind=payment, status=delivered, external_id=pi_dev_…, payload.simulated=true).GET /v1/billing/productsagain →compliancePack:true.Test plan
pnpm test— 30 specs across 6 suites. All passing.tests/api/billing.test.ts— products shape × 2, PaymentIntent happy/409-already-entitled/400-bad-sku/502-stripe-errortests/api/stripe-webhook.test.ts— unsigned-400, forged-signature-400, succeeded-flips-entitlement-and-emits-SSE, failed-persists-action-no-flip, unknown-event-200tests/web/stripe-modal.test.tsx— loads product, already-entitled short-circuit, full payment flow + SSE success, Shift+Enter fallback, retrypnpm typecheckclean across all three workspaces.pnpm --filter @redline/web dev→ openUpgrademodal → submit a test card or hit Shift+Enter → see the success state flip via SSE. (Easy to repro; left for reviewer.)stripe listen --forward-to localhost:8787/webhooks/stripe, copywhsec_...into.env.local, do one real card-confirm flow to verify the real webhook path.Stacking note
This PR is stacked on #2 (
base: frex22/add-vendor-first-scan). When #7 merges tomain, retarget this PR tomainand rebase.🤖 Generated with Claude Code