rebranding
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# 13. Backend Router — Subscriptions, Billing, and Stripe Webhooks
|
||||
|
||||
meta:
|
||||
id: kordant-unified-restructure-13
|
||||
feature: kordant-unified-restructure
|
||||
priority: P1
|
||||
depends_on: [kordant-unified-restructure-11]
|
||||
tags: [backend, trpc, billing, stripe, api]
|
||||
|
||||
objective:
|
||||
- Build the tRPC router for subscription management and Stripe billing integration. Port logic from `packages/shared-billing/` and `packages/api/src/routes/subscription.routes.ts` into a unified `billing` router.
|
||||
|
||||
deliverables:
|
||||
- `web/src/server/api/routers/billing.ts` — Billing router with procedures:
|
||||
- `billing.getSubscription` — `protectedProcedure` returning current subscription with tier
|
||||
- `billing.createCheckoutSession` — `protectedProcedure` creating Stripe checkout for plan upgrade
|
||||
- `billing.createPortalSession` — `protectedProcedure` creating Stripe Customer Portal
|
||||
- `billing.cancelSubscription` — `protectedProcedure` scheduling cancellation at period end
|
||||
- `billing.reactivateSubscription` — `protectedProcedure` reactivating a canceled subscription
|
||||
- `billing.listInvoices` — `protectedProcedure` returning Stripe invoices for user
|
||||
- `billing.webhook` — `publicProcedure` (or raw endpoint) handling Stripe webhook events
|
||||
- `web/src/server/services/billing.service.ts` — Business logic:
|
||||
- `getOrCreateCustomer(userId, email)` — Stripe customer management
|
||||
- `createCheckoutSession(userId, priceId, successUrl, cancelUrl)`
|
||||
- `createPortalSession(customerId, returnUrl)`
|
||||
- `handleWebhookEvent(event)` — processes `checkout.session.completed`, `invoice.paid`, `invoice.payment_failed`, `customer.subscription.updated`, `customer.subscription.deleted`
|
||||
- `updateSubscriptionInDB(subscriptionId, stripeEventData)` — syncs Stripe state to Drizzle
|
||||
- `web/src/routes/api/stripe/webhook.ts` — Raw SolidStart API route for Stripe webhooks:
|
||||
- Verifies Stripe signature using `stripe.webhooks.constructEvent`
|
||||
- Forwards to billing service
|
||||
- Zod schemas in `web/src/server/api/schemas/billing.ts`
|
||||
- Stripe client initialization in `web/src/server/stripe.ts`
|
||||
|
||||
steps:
|
||||
1. Install `stripe` npm package in `web/`.
|
||||
2. Create `web/src/server/stripe.ts`:
|
||||
- Initialize Stripe client with `process.env.STRIPE_SECRET_KEY`
|
||||
- Export `stripe` instance
|
||||
3. Create `web/src/server/services/billing.service.ts`:
|
||||
- `getOrCreateCustomer`: query DB for existing `stripeCustomerId`, or create via Stripe API and save to DB
|
||||
- `createCheckoutSession`: create Stripe Checkout session with `subscription` mode, return URL
|
||||
- `createPortalSession`: create Stripe Billing Portal session
|
||||
- `handleWebhookEvent`: switch on event type:
|
||||
- `checkout.session.completed` → create/update subscription record, set tier
|
||||
- `invoice.paid` → update subscription status to active
|
||||
- `invoice.payment_failed` → update status to past_due, send notification
|
||||
- `customer.subscription.updated` → sync tier, status, period dates
|
||||
- `customer.subscription.deleted` → mark as canceled
|
||||
4. Create `web/src/server/api/routers/billing.ts`:
|
||||
- `getSubscription`: query user's active subscription, include tier details
|
||||
- `createCheckoutSession`: validate price ID against allowed prices, call service, return session URL
|
||||
- `createPortalSession`: call service, return portal URL
|
||||
- `cancelSubscription`: update Stripe subscription to `cancel_at_period_end`, update DB
|
||||
- `reactivateSubscription`: remove `cancel_at_period_end`, update DB
|
||||
- `listInvoices`: fetch Stripe invoices for customer, return paginated list
|
||||
5. Create `web/src/routes/api/stripe/webhook.ts`:
|
||||
- SolidStart API route (not tRPC — Stripe needs raw body for signature verification)
|
||||
- Read raw body from request
|
||||
- Verify signature with `STRIPE_WEBHOOK_SECRET`
|
||||
- Call `billingService.handleWebhookEvent`
|
||||
- Return 200 for success, 400 for invalid signature, 500 for processing errors
|
||||
6. Define Zod schemas for all inputs (priceId, successUrl, etc.).
|
||||
7. Wire router into `web/src/server/api/root.ts`.
|
||||
8. Write unit tests for billing service (mock Stripe API calls).
|
||||
|
||||
steps:
|
||||
- Unit: `getOrCreateCustomer` returns existing customer or creates new one
|
||||
- Unit: `handleWebhookEvent` correctly updates subscription status for each event type
|
||||
- Unit: Stripe signature verification rejects invalid signatures
|
||||
- Integration: `billing.getSubscription` returns subscription for authenticated user
|
||||
- Integration: Checkout session creation returns valid Stripe URL
|
||||
|
||||
acceptance_criteria:
|
||||
- [ ] `billing.getSubscription` returns current subscription with tier and status
|
||||
- [ ] `billing.createCheckoutSession` creates a Stripe checkout session and returns the URL
|
||||
- [ ] `billing.createPortalSession` creates a Stripe customer portal session
|
||||
- [ ] `billing.cancelSubscription` sets `cancelAtPeriodEnd` in Stripe and DB
|
||||
- [ ] `billing.reactivateSubscription` removes cancellation flag
|
||||
- [ ] Stripe webhook endpoint correctly handles all relevant event types
|
||||
- [ ] Webhook signature verification prevents spoofed requests
|
||||
- [ ] Subscription tier changes correctly gate features (enforced in other routers)
|
||||
|
||||
validation:
|
||||
- Use Stripe CLI to trigger test webhooks: `stripe listen --forward-to localhost:3000/api/stripe/webhook`
|
||||
- Verify webhook handler responds 200 and updates DB correctly
|
||||
- Test checkout flow with Stripe Test Mode
|
||||
- Run `cd web && pnpm test` for billing unit tests
|
||||
|
||||
notes:
|
||||
- Reference legacy: `packages/shared-billing/src/`, `packages/api/src/routes/subscription.routes.ts`
|
||||
- Store Stripe price IDs in environment variables or a config file:
|
||||
- `STRIPE_PRICE_BASIC`, `STRIPE_PRICE_PLUS`, `STRIPE_PRICE_PREMIUM`
|
||||
- The webhook endpoint MUST receive the raw request body, not parsed JSON. In SolidStart, use `request.text()` before any parsing.
|
||||
- Ensure idempotency: webhook events may be delivered multiple times. Use Stripe event ID to deduplicate.
|
||||
- For the tier enum in Drizzle schema, ensure it matches Stripe product metadata or price IDs.
|
||||
- Consider adding a `billing.history` procedure that returns payment history from Stripe invoices.
|
||||
Reference in New Issue
Block a user