# 13. Backend Router — Subscriptions, Billing, and Stripe Webhooks

meta:
  id: shieldai-unified-restructure-13
  feature: shieldai-unified-restructure
  priority: P1
  depends_on: [shieldai-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.
