feat(billing): Stripe Checkout + Customer Portal + signed webhook
Some checks failed
Deploy to Production / deploy (push) Failing after 46s
Some checks failed
Deploy to Production / deploy (push) Failing after 46s
- @bmm/api: stripe@22 SDK, plan-aware price-id lookup, Redis-backed event
idempotency (7d TTL covers Stripe's retry window), startup warning when
STRIPE_PRICE_* env vars contain product ids (prod_) by mistake
- routes/billing.ts:
POST /v1/billing/checkout-session → Stripe-hosted Checkout, SEPA+card,
auto-VAT via Stripe Tax, tax_id
collection for B2B, address required
POST /v1/billing/portal → Customer Portal session
GET /v1/billing/status → drives the settings/billing UI
POST /v1/billing/webhook → signed, idempotent, handles
checkout.session.completed,
subscription.{created,updated,deleted},
invoice.{paid,payment_failed}
- index.ts: rawBody-aware JSON parser so Stripe signature verify gets the
exact payload bytes
- web: /settings/billing page (status, upgrade flow, manage-billing portal,
auto-checkout when arriving with ?tier=… from the pricing CTAs), pricing
page CTAs point to /settings/billing?tier=…
- Payment-failure path: suspend org only after 3rd failed attempt (Stripe
Smart Retries handles the soft-retries). Suspended orgs keep their running
servers but cannot create new ones (enforcement is in /v1/servers POST as
a follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -56,6 +56,9 @@ importers:
|
||||
jose:
|
||||
specifier: 5.9.6
|
||||
version: 5.9.6
|
||||
stripe:
|
||||
specifier: ^22.1.1
|
||||
version: 22.1.1(@types/node@22.10.2)
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
@@ -1982,6 +1985,15 @@ packages:
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
stripe@22.1.1:
|
||||
resolution: {integrity: sha512-cmodIYP27tBkJ8G7DuGgWw0PFuemlFZbuF3Wwr1TrjFjUa3T7NIgCe6TVwX8BO2ynu+xtTuDGfHafNDCPt9lXA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -3616,6 +3628,10 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
stripe@22.1.1(@types/node@22.10.2):
|
||||
optionalDependencies:
|
||||
'@types/node': 22.10.2
|
||||
|
||||
styled-jsx@5.1.6(react@19.0.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
|
||||
Reference in New Issue
Block a user