Skip to content

Stripe Compliance Pack purchase flow · closes #3#8

Open
Frex22 wants to merge 1 commit into
frex22/add-vendor-first-scanfrom
frex22/stripe-compliance-pack
Open

Stripe Compliance Pack purchase flow · closes #3#8
Frex22 wants to merge 1 commit into
frex22/add-vendor-first-scanfrom
frex22/stripe-compliance-pack

Conversation

@Frex22

@Frex22 Frex22 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

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), returns clientSecret + publishableKey. Verified live: pi_3TaJSxJXawLng5dj03B8WmHX is queryable via the Stripe API on acct_1TaIsMJXawLng5dj.
  • POST /webhooks/stripe — raw-body signature verification via stripe.webhooks.constructEvent.
    • payment_intent.succeeded → flips org.compliancePack, persists Action(kind:"payment", status:"delivered"), emits org.entitlements.changed SSE
    • payment_intent.payment_failed → persists Action(status:"failed"), no entitlement change
    • Unknown event types → 200 no-op (handoff/API §07)
  • POST /v1/billing/simulate-successdev-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 + idempotent ensureStripeProducts() boot bootstrap.
  • apps/api/src/db/actions.tsActionStore interface + ClickHouseActionStore writing the existing actions table (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 for org.entitlements.changed.

Web

  • apps/web/src/screens/StripeModal.tsx@stripe/react-stripe-js with Elements + PaymentElement. States: loading-productready-to-paycreating-intentelements-readyprocessingsuccess (driven by SSE org.entitlements.changed) → failure with retry. Already-entitled orgs short-circuit straight to success.
  • apps/web/src/lib/api.ts — extends with getBillingProducts(), createPaymentIntent(), simulateSuccess() (no rewrite of [Sprint 1 | Frontend/API | Frex22] Add Vendor flow with first-scan subscription #2 surface).
  • apps/web/src/lib/stream.tsuseEntitlements hook 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 — adds EntitlementsChangedEvent to the SseEvent union.

Caveats reviewers should know

  • Real webhook flow (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's generateTestHeaderString against constructEvent (no network).
  • STRIPE_WEBHOOK_SECRET is currently a dev placeholder in .env.local. Replace with the real whsec_... printed by stripe listen --forward-to localhost:8787/webhooks/stripe before demo.
  • /v1/billing/simulate-success is dev-only — returns 404 when NODE_ENV=production. This is the explicit Runbook F5 fallback; the modal's Shift+Enter calls it.
  • The compliance-pack product 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); actions table reused.
  • GET /v1/billing/products → 200 with compliancePack:false initially.
  • POST /v1/billing/payment-intents { sku:"compliance-pack" } → 200 with real pi_3TaJSxJXawLng5dj03B8WmHX (verified via Stripe API).
  • POST /v1/billing/simulate-success → 200, compliancePack flips to true, actions row inserted in ClickHouse (kind=payment, status=delivered, external_id=pi_dev_…, payload.simulated=true).
  • GET /v1/billing/products again → 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-error
    • tests/api/stripe-webhook.test.ts — unsigned-400, forged-signature-400, succeeded-flips-entitlement-and-emits-SSE, failed-persists-action-no-flip, unknown-event-200
    • tests/web/stripe-modal.test.tsx — loads product, already-entitled short-circuit, full payment flow + SSE success, Shift+Enter fallback, retry
  • pnpm typecheck clean across all three workspaces.
  • Live end-to-end smoke against real Stripe (test mode) and ClickHouse Cloud.
  • Browser smoke: pnpm --filter @redline/web dev → open Upgrade modal → submit a test card or hit Shift+Enter → see the success state flip via SSE. (Easy to repro; left for reviewer.)
  • Pre-demo: install Stripe CLI, run stripe listen --forward-to localhost:8787/webhooks/stripe, copy whsec_... 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 to main, retarget this PR to main and rebase.

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant