# Stripe Customer Portal Integration

**Date:** 2025-01-11 (lifecycle banners added 2026-06)
**Status:** ✅ Implemented
**Version:** 1.1

---

## Table of Contents

1. [Overview](#overview)
2. [How It Works](#how-it-works)
3. [Where It's Hosted](#where-its-hosted)
4. [What Dealers Can Do](#what-dealers-can-do)
5. [In-App Lifecycle Banners](#in-app-lifecycle-banners)
6. [Do Dealers Need a Stripe Account?](#do-dealers-need-a-stripe-account)
7. [Setup Required (Action Items)](#setup-required-action-items)
8. [The Complete User Flow](#the-complete-user-flow)
9. [Technical Implementation](#technical-implementation)
10. [Security](#security)
11. [Troubleshooting](#troubleshooting)

---

## Overview

The **Stripe Customer Portal** is a pre-built, hosted billing management interface provided by Stripe. Instead of building our own UI for payment methods, invoices, and billing, we redirect dealers to Stripe's secure portal where they can manage everything themselves.

### What We Built

We created a **subscription management page** (`/dashboard/subscription`) that:

- Displays dealer's current plan and status
- Shows subscription information and business details
- Provides a button to access Stripe's Customer Portal
- Handles authentication and creates secure portal sessions

### Benefits

✅ **Self-Service** - Dealers manage billing without contacting support
✅ **Secure** - PCI compliant, Stripe handles all payment data
✅ **Professional** - Industry-standard, recognizable interface
✅ **Fast Implementation** - 1 day vs 2-3 weeks for custom build
✅ **No Maintenance** - Stripe updates portal automatically
✅ **Trusted** - Dealers recognize and trust Stripe branding

---

## How It Works

### The Simple Version

1. Dealer logs into **your site** (yourdomain.com)
2. Dealer navigates to "Subscription" page
3. Dealer clicks "Manage Billing & Invoices" button
4. **Your server** creates a temporary portal session with Stripe
5. Dealer redirected to **Stripe's portal** (billing.stripe.com)
6. Dealer manages their billing on **Stripe's site**
7. When done, dealer clicks "Back" and returns to **your site**

### Key Point

**The billing portal is hosted by Stripe, not on your server.**

---

## Where It's Hosted

### Your Server (yourdomain.com)

**What's hosted here:**

- `/dashboard/subscription` - Your subscription information page
- `/api/subscription/portal` - API endpoint that creates portal sessions
- `/api/dealers/current` - API endpoint that fetches dealer info

**What it does:**

- Displays dealer's current plan and status
- Authenticates the dealer
- Creates secure portal sessions
- Redirects to Stripe

### Stripe's Servers (billing.stripe.com)

**What's hosted here:**

- The actual Customer Portal
- Payment method management
- Invoice viewing and downloads
- Billing address updates

**What it does:**

- Handles all payment data (PCI compliant)
- Manages payment methods securely
- Generates invoice PDFs
- Processes subscription changes

### The Complete URL Flow

```
1. Dealer on:    https://yourdomain.com/dashboard/subscription
2. Clicks button
3. Your API:     https://yourdomain.com/api/subscription/portal
4. Redirects to: https://billing.stripe.com/p/session/abc123...
5. Dealer manages billing (on Stripe)
6. Returns to:   https://yourdomain.com/dashboard/subscription
```

---

## What Dealers Can Do

### In the Stripe Customer Portal

#### ✅ Update Payment Methods

- Add new credit/debit card
- Remove old cards
- Set default payment method
- All PCI compliant (Stripe handles card data securely)

#### ✅ View Invoice History

- See all past invoices
- Download PDF receipts
- View payment dates and amounts
- See billing periods

#### ✅ Update Billing Address

- Change billing address
- Update ZIP/postal code
- Update country
- Update tax information

#### ✅ Cancel Subscription (Optional - You Control This)

- Cancel subscription immediately
- Cancel at end of billing period
- **Important:** You decide in Stripe Dashboard if this option is available

### What Dealers CANNOT Do (By Design)

❌ **Change Subscription Tier** (upgrade/downgrade)

- Requires custom implementation for prorated billing
- Future enhancement (Task #6)

❌ **Delete Their Account**

- Requires data export and GDPR compliance
- Deferred to future Account Settings page (Task #7)

❌ **View Usage Metrics**

- Requires custom implementation
- Future enhancement (Task #6)

---

## In-App Lifecycle Banners

The subscription page (`app/dashboard/subscription/page.tsx`) renders a status-specific banner above the billing portal card. The portal handles *self-managed* billing (cards, invoices, cancellation); these banners drive *recovery* when a dealer is no longer in good standing. Behavior is status-gated:

| Dealer status       | Banner                | Self-serve CTA                                                                 |
| ------------------- | --------------------- | ----------------------------------------------------------------------------- |
| `active`            | none                  | Normal Billing Management card (portal)                                        |
| `payment_failed`    | "Action Required"     | **Update payment method** → opens Stripe portal (dunning rescue, PR #891)      |
| `cancelled_pending` | "Cancellation Pending"| none (site serves until period end; contact support to reverse)               |
| `cancelled`         | "Subscription Cancelled" | **Reactivate `<tier>` plan** → `POST /api/subscription/reactivate` (PR #891) |
| `suspended`         | "Account Suspended"   | **none** - contact-support link only (PR #894)                                 |

### Dunning rescue (`payment_failed`)

When an invoice fails, the webhook sets `status = payment_failed`, the site goes offline (ISR pages purged), and the banner exposes an **Update payment method** button that opens the Stripe portal. Stripe keeps retrying the failed payment on its dunning schedule and will charge the updated card on its next attempt; to restore service immediately, the dealer pays the open invoice in the portal. This rescue CTA is for `payment_failed` dealers only.

### Reactivation (`cancelled`)

Cancellation deletes the Stripe subscription but preserves the customer + billing history. The `cancelled` banner offers a **Reactivate `<tier>` plan** button (plus a monthly/annual radio for non-starter tiers - starter is annual-only) that hits `POST /api/subscription/reactivate` (`app/api/subscription/reactivate/route.ts`). That endpoint reactivates on the dealer's **prior** tier (server-trusted, read from the DB), starts a **new** Stripe Checkout session on the existing customer, and has a server-side double-charge guard. On success the dealer returns to `/dashboard/subscription?reactivated=1`, where the CTA is suppressed until the `customer.subscription.created` webhook flips them back to served. See `docs/TIER_TRANSITIONS.md` for the full `cancelled → active` gate.

### Suspended: no self-serve rescue (PR #894)

A `suspended` dealer sees a "contact support" message with **no** billing CTA. Suspension is an admin action for accounts in violation (and the terminal dunning state: Stripe unpaid/paused → suspended), so recovery is deliberately gated through support - we don't hand a misbehaving account a one-click pay-to-restore path. The portal endpoint (`/api/subscription/portal`) is itself not status-gated; the suspended banner simply exposes no link to it. PR #894 removed the pay-to-restore CTA that the dunning work had briefly given suspended dealers.

---

## Do Dealers Need a Stripe Account?

### Short Answer: NO

Dealers do **NOT** need to create a Stripe account or log into Stripe.

### How Authentication Works

#### The "Magic Link" Approach

When a dealer clicks "Manage Billing & Invoices":

1. **Your server creates a temporary session:**

   ```typescript
   const portalSession = await stripe.billingPortal.sessions.create({
     customer: dealer.stripeCustomerId,
     return_url: 'https://yourdomain.com/dashboard/subscription',
   });
   ```

2. **This returns a "magic link":**
   - URL: `https://billing.stripe.com/p/session/abc123...`
   - Single-use (cannot be reused)
   - Expires in 60 minutes
   - Grants temporary access to ONLY their billing info

3. **Dealer is redirected to that unique URL**

4. **Stripe recognizes the session and shows their info**

5. **No login, password, or Stripe account needed**

### The Trust Chain

```
┌────────────────────────────────────────────┐
│ 1. Dealer authenticates with YOUR site    │
│    (Google/Apple OAuth via NextAuth)      │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 2. Your server verifies their identity    │
│    (NextAuth session)                      │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 3. Your server looks up Stripe customer   │
│    (from your database)                    │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 4. Your server creates portal session     │
│    (Stripe API, authenticated with keys)  │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 5. Stripe trusts YOUR authentication      │
│    (you vouched for this dealer)           │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 6. Dealer accesses portal via magic link  │
│    (no Stripe account needed)              │
└────────────────────────────────────────────┘
```

**The dealer never authenticates directly with Stripe. They authenticate with you, and you vouch for them to Stripe.**

### What Dealers See

#### They DON'T See:

- ❌ Stripe login screen
- ❌ "Create Stripe account" form
- ❌ Username/password fields
- ❌ Any mention of needing a Stripe account

#### They DO See:

- ✅ Immediate access to billing portal
- ✅ "Powered by Stripe" branding
- ✅ Their payment methods and invoices
- ✅ A "Back to [Your Company]" button

### What Gets Created During Checkout?

When a dealer subscribes (goes through Stripe Checkout):

#### Automatically Created in Stripe:

1. **Stripe Customer** (`cus_abc123...`)
   - Email: dealer's email
   - Name: dealer's business name
   - Metadata: your custom data

2. **Stripe Subscription** (`sub_xyz789...`)
   - Linked to the customer
   - Has a price/plan
   - Has a payment method attached

3. **Payment Method** (`pm_card456...`)
   - Credit card details (encrypted by Stripe)
   - Linked to the customer

#### Stored in Your Database:

```typescript
// prisma.dealer
{
  stripeCustomerId: "cus_abc123...",      // Links to Stripe
  stripeSubscriptionId: "sub_xyz789...",
  subscriptionTier: "professional",
  status: "active"
}
```

**Important:** No Stripe user account is created. Just customer records in Stripe's system that link to your dealers.

---

## Setup Required (Action Items)

### Prerequisites

Before the Customer Portal will work, you need to configure it in your Stripe Dashboard.

### Step 1: Enable Customer Portal

1. **Log into Stripe Dashboard:** https://dashboard.stripe.com
2. **Navigate to:** Settings → Billing → Customer Portal
3. **Toggle ON:** "Customer Portal"

### Step 2: Configure Portal Features

Enable the features you want dealers to access:

#### Required (Enable These):

- ✅ **Allow customers to update payment methods**
  - Dealers can add/remove cards
  - Set default payment method

- ✅ **Allow customers to view invoice history**
  - See past invoices
  - Download PDF receipts

- ✅ **Allow customers to update billing information**
  - Update billing address
  - Update tax information

#### Optional (Your Decision):

- ⚠️ **Allow customers to cancel subscriptions**
  - **Enable:** Dealers can self-cancel anytime
  - **Disable:** Dealers must contact support to cancel
  - **Recommended:** Enable with "Cancel at period end" to allow graceful cancellations

- ⚠️ **Allow customers to pause subscriptions**
  - **Enable:** Dealers can pause for a period (if you offer this)
  - **Disable:** No pause option
  - **Recommended:** Disable unless you have a specific use case

### Step 3: Configure Branding

Make the portal match your brand:

1. **Upload Logo**
   - Your company logo
   - Appears at top of portal

2. **Set Brand Colors**
   - Primary color (buttons, links)
   - Background color
   - Text color

3. **Set Icon**
   - Favicon for portal page

### Step 4: Configure Business Information

1. **Support Email**
   - Where dealers can reach you for help
   - Example: support@yourdomain.com

2. **Privacy Policy URL**
   - Link to your privacy policy
   - Example: https://yourdomain.com/privacy

3. **Terms of Service URL**
   - Link to your terms
   - Example: https://yourdomain.com/terms

### Step 5: Test the Portal

1. **Create a test subscription:**

   ```bash
   # Use Stripe test mode
   # Use test card: 4242 4242 4242 4242
   ```

2. **Access portal as test dealer:**
   - Log into your site with test account
   - Navigate to /dashboard/subscription
   - Click "Manage Billing & Invoices"
   - Verify portal loads correctly

3. **Test portal features:**
   - Update payment method
   - View invoices
   - Update billing address
   - Test cancellation (if enabled)

### Step 6: Go Live

1. **Switch Stripe to Live Mode**
2. **Verify portal settings in Live Mode**
   - Portal settings are separate for Test/Live modes
   - Configure Live mode portal identically to Test mode

3. **Update environment variables:**
   ```bash
   # .env.production
   STRIPE_SECRET_KEY=sk_live_...  # Live secret key
   STRIPE_PUBLISHABLE_KEY=pk_live_...  # Live publishable key
   ```

### Configuration Checklist

Use this checklist to ensure everything is set up:

```
Portal Configuration:
- [ ] Customer Portal enabled in Stripe Dashboard
- [ ] Payment method updates enabled
- [ ] Invoice history viewing enabled
- [ ] Billing information updates enabled
- [ ] Cancellation policy decided (enable/disable)
- [ ] Logo uploaded
- [ ] Brand colors configured
- [ ] Support email set
- [ ] Privacy policy URL set
- [ ] Terms of service URL set
- [ ] Tested in Stripe Test Mode
- [ ] Configured in Stripe Live Mode
- [ ] Environment variables updated for production
```

---

## The Complete User Flow

### Step 1: Dealer on Subscription Page

**URL:** `https://yourdomain.com/dashboard/subscription`

**Page displays:**

```
┌─────────────────────────────────────────┐
│  Subscription & Billing                 │
├─────────────────────────────────────────┤
│  Current Subscription                   │
│                                         │
│  Plan: Professional                     │
│  Status: Active ✓                       │
│                                         │
│  Features:                              │
│  ✓ Unlimited Lead Pages                │
│  ✓ Advanced Analytics                  │
│  ✓ Priority Support                    │
│                                         │
│  ───────────────────────────────────    │
│                                         │
│  Billing Management                     │
│                                         │
│  ✓ Update Payment Method               │
│  ✓ View Invoice History                │
│  ✓ Update Billing Address              │
│                                         │
│  [Manage Billing & Invoices] ← Button  │
│                                         │
│  You'll be redirected to our secure    │
│  billing portal powered by Stripe      │
└─────────────────────────────────────────┘
```

### Step 2: Dealer Clicks Button

**Button click triggers:**

```typescript
const handlePortalAccess = () => {
  setPortalLoading(true); // Show spinner
  window.location.href = '/api/subscription/portal';
};
```

**Button displays:** `"Loading Portal..."`

### Step 3: Server Creates Portal Session

**API Endpoint:** `GET /api/subscription/portal`

**Server logic:**

```typescript
// Note: Rate limiting is handled at the Cloudflare edge

// 1. Verify authentication
const session = await getServerSession(authOptions);
if (!session?.user?.id) return unauthorized();

// 2. Fetch dealer from database
const dealer = await prisma.dealer.findUnique({
  where: { userId: session.user.id },
  select: { stripeCustomerId: true, ... }
});
if (!dealer?.stripeCustomerId) return notFound();

// 3. Create Stripe portal session
const portalSession = await stripe.billingPortal.sessions.create({
  customer: dealer.stripeCustomerId,
  return_url: `${origin}/dashboard/subscription`,
});

// 4. Redirect to Stripe portal
return NextResponse.redirect(portalSession.url);
```

### Step 4: Dealer Redirected to Stripe

**URL:** `https://billing.stripe.com/p/session/abc123...`

**Stripe portal displays:**

```
┌─────────────────────────────────────────┐
│  🔒 Secure Billing Portal               │
│  Powered by Stripe                      │
│                                         │
│  Payment Methods                        │
│  ├─ Visa •••• 4242 [Default]           │
│  │  Expires 12/25                       │
│  └─ [+ Add Payment Method]              │
│                                         │
│  Invoices                               │
│  ├─ Jan 2025 - $49.00 [Download PDF]   │
│  ├─ Dec 2024 - $49.00 [Download PDF]   │
│  └─ Nov 2024 - $49.00 [Download PDF]   │
│                                         │
│  Billing Address                        │
│  123 Main St                            │
│  Minneapolis, MN 55401                  │
│  [Edit Address]                         │
│                                         │
│  Subscription                           │
│  Professional Plan - $49/month          │
│  Next billing date: Feb 15, 2025       │
│  [Cancel Subscription] ← Optional       │
│                                         │
│  [← Back to Your Company]               │
└─────────────────────────────────────────┘
```

### Step 5: Dealer Manages Billing

**Available actions:**

- Add new payment method
- Set default card
- Remove old cards
- View/download invoices
- Update billing address
- Cancel subscription (if enabled)

**All changes are instant** - no waiting for approval.

### Step 6: Dealer Returns to Your Site

**Clicks:** "Back to [Your Company]" button

**Redirects to:** `https://yourdomain.com/dashboard/subscription`

**Your page refreshes** and shows updated information (if any changes were made).

### Step 7: Webhooks Notify Your Server

If dealer made changes, Stripe sends webhooks:

**Example: Dealer updates card**

```
1. Dealer adds new card in portal
2. Stripe sends webhook: payment_method.attached
3. Your webhook handler: /api/webhooks/stripe
4. You can log, send email, etc.
```

**Example: Dealer cancels subscription**

```
1. Dealer clicks "Cancel" in portal
2. Stripe sends webhook: customer.subscription.deleted
3. Your webhook handler (app/api/webhooks/stripe/route.ts) updates database:
   - dealer.status = 'cancelled'
   - dealer.stripeSubscriptionId = null
   - dealer.hasLeadForm = false
4. Side effects on first transition to cancelled:
   - ISR pages purged (revalidateAllDealerPages) so the origin stops serving the site
   - Certbot cert revoked if certExpiresAt is set (revokeCertbotCertIfPresent), so the
     renewal timer stops failing on a dark domain (PR #866); certExpiresAt cleared only
     on revoke success
5. Next page load shows "Cancelled" status — with a self-serve Reactivate CTA (PR #891)
```

> Cancellation is **not** terminal. A `cancelled` dealer can self-serve reactivate from the subscription page (a brand-new Stripe subscription on the same customer). See [In-App Lifecycle Banners](#in-app-lifecycle-banners) below and `docs/TIER_TRANSITIONS.md`.

---

## Technical Implementation

### Files Created

#### 1. Portal API Endpoint

**File:** `app/api/subscription/portal/route.ts` (131 lines)

**Purpose:** Creates Stripe portal session and redirects dealer

**Key features:**

- Authentication via NextAuth
- Rate limiting (30 requests/min per IP)
- Ownership verification
- Comprehensive error handling
- Logging for audit trail

#### 2. Subscription Page

**File:** `app/dashboard/subscription/page.tsx` (298 lines)

**Purpose:** Displays subscription info and portal access button

**Key features:**

- Client-side React component
- Fetches dealer info from API
- Status-specific alerts (payment failed, suspended, cancelled)
- Loading states
- Mobile responsive
- Accessibility (ARIA labels, keyboard navigation)

#### 3. Dealer Info API

**File:** `app/api/dealers/current/route.ts` (76 lines)

**Purpose:** Returns authenticated dealer's information

**Key features:**

- GET endpoint
- Authentication required
- Returns subscription tier, status, business info
- Security: Excludes Stripe IDs from response

### Files Modified

#### 4. Dashboard

**File:** `app/dashboard/page.tsx`

**Changes:** Added "Subscription" navigation link (lines 92-111)

#### 5. Proxy

**File:** `proxy.ts`

**Changes:**

- Added `/api/subscription` to protected routes (line 31)
- Added `/api/subscription/:path*` to matcher (line 74)

### API Endpoints

#### GET /api/subscription/portal

**Purpose:** Create and redirect to Stripe Customer Portal

**Authentication:** Required (NextAuth session)

**Rate Limit:** 30 requests/min per IP

**Request:** No body required

**Response:** 307 redirect to Stripe portal

**Error Codes:**

- `401` - Not authenticated
- `404` - No dealer account or no Stripe customer ID
- `429` - Rate limit exceeded
- `500` - Server error or Stripe API error

**Example request:**

```bash
curl -X GET https://yourdomain.com/api/subscription/portal \
  -H "Cookie: next-auth.session-token=..." \
  -L
```

#### GET /api/dealers/current

**Purpose:** Fetch current authenticated dealer information

**Authentication:** Required (NextAuth session)

**Rate Limit:** None (read-only)

**Request:** No body required

**Response:** `200 OK` with dealer info

**Response body:**

```json
{
  "id": "clxxxxx",
  "subdomain": "bobsoil",
  "domain": "bobsoil.com",
  "subscriptionTier": "professional",
  "status": "active",
  "businessName": "Bob's Oil Service",
  "phone": "(555) 123-4567",
  "address": "123 Main St",
  "city": "Minneapolis",
  "state": "MN",
  "zip": "55401",
  "country": "US",
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": "2025-01-11T00:00:00.000Z"
}
```

**Note:** Excludes `stripeCustomerId` and `stripeSubscriptionId` for security.

**Error Codes:**

- `401` - Not authenticated
- `404` - No dealer account found
- `500` - Server error

### Database Schema

**Table:** `Dealer`

**Relevant fields:**

```prisma
model Dealer {
  id                    String   @id @default(cuid())
  userId                String   @unique
  stripeCustomerId      String?  @unique
  stripeSubscriptionId  String?  @unique
  subscriptionTier      String   @default("starter")
  status                String   @default("pending")
  businessName          String
  phone                 String
  address               String
  city                  String
  state                 String
  zip                   String
  country               String   @default("US")
  createdAt             DateTime @default(now())
  updatedAt             DateTime @updatedAt
}
```

**Key relationships:**

- `userId` → links to NextAuth `User.id`
- `stripeCustomerId` → links to Stripe Customer
- `stripeSubscriptionId` → links to Stripe Subscription

---

## Security

### Authentication Flow

```
┌────────────────────────────────────────────┐
│ 1. Dealer must be logged in (NextAuth)    │
│    - Google OAuth or Apple OAuth           │
│    - Session stored in JWT                 │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 2. Portal endpoint verifies session       │
│    - getServerSession(authOptions)         │
│    - Returns 401 if not authenticated      │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 3. Server fetches dealer from database    │
│    - WHERE userId = session.user.id        │
│    - Ensures dealer owns this account      │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 4. Server creates portal session          │
│    - Authenticated with Stripe API keys    │
│    - For specific customer ID only         │
└────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────┐
│ 5. Dealer redirected to unique URL        │
│    - Single-use session token              │
│    - Expires in 60 minutes                 │
└────────────────────────────────────────────┘
```

### Security Features

#### 1. Authentication Required

```typescript
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
```

Only authenticated dealers can access portal.

#### 2. Ownership Verification

```typescript
const dealer = await prisma.dealer.findUnique({
  where: { userId: session.user.id },
});
```

Dealers can only access their own billing information.

#### 3. Single-Use Portal Sessions

- Each portal session is single-use
- Cannot be shared or reused
- Expires after 60 minutes
- New session required for each access

#### 4. No Sensitive Data Exposed

The `/api/dealers/current` endpoint excludes:

- `stripeCustomerId` (only used server-side)
- `stripeSubscriptionId` (only used server-side)

These IDs are never sent to the frontend.

#### 5. Rate Limiting

- 30 requests/min per IP address
- Prevents abuse and automated attacks
- Legitimate users won't hit this limit

#### 6. Stripe Signature Verification

Webhooks from Stripe are verified:

```typescript
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
```

Prevents fake webhook attacks.

#### 7. HTTPS Only

- Portal sessions only work over HTTPS
- Stripe requires secure connections
- Production must use SSL/TLS

### What Stripe Validates

When dealer accesses portal:

1. **Session token is valid**
   - Not expired (< 60 minutes old)
   - Not already used
   - Matches expected format

2. **Customer ID matches session**
   - Portal session was created for this customer
   - Cannot access other customers' data

3. **Session is authenticated**
   - Created by authenticated API call
   - Your server's Stripe API keys validated

### Edge Cases

#### Scenario: Dealer Bookmarks Portal URL

**What happens:**

```
1. Dealer accesses portal
2. URL: https://billing.stripe.com/p/session/abc123
3. Dealer bookmarks it
4. Next week, dealer clicks bookmark
```

**Result:**

```
❌ Error: "This link has expired"
```

**Why:** Session tokens expire and are single-use

**Solution:** Dealer must return to your site and click button again

#### Scenario: Dealer Shares Portal Link

**What happens:**

```
1. Dealer A accesses portal
2. URL: https://billing.stripe.com/p/session/abc123
3. Dealer A shares link with Dealer B
4. Dealer B clicks link
```

**Result:**

```
❌ Error: "This link has expired" or "Invalid session"
```

**Why:** Sessions are single-use and tied to specific customer

**Security:** Even if link is fresh, it's tied to Dealer A's customer ID

#### Scenario: Deleted Stripe Customer

**What happens:**

```
1. Customer deleted in Stripe Dashboard
2. Dealer clicks "Manage Billing"
3. Server tries to create portal session
```

**Result:**

```
❌ Stripe API error: "No such customer"
```

**Handling:**

```typescript
catch (error) {
  if (error instanceof Stripe.errors.StripeError) {
    logger.error({ stripeErrorType: error.type });
    return NextResponse.json(
      { error: 'Billing account not found' },
      { status: 404 }
    );
  }
}
```

---

## Troubleshooting

### Issue: Portal Button Does Nothing

**Symptoms:**

- Click "Manage Billing & Invoices"
- Nothing happens
- No redirect

**Possible causes:**

1. **JavaScript error in browser**
   - **Check:** Browser console for errors
   - **Fix:** Clear cache, refresh page

2. **Session expired**
   - **Check:** User still logged in?
   - **Fix:** Re-authenticate with OAuth

3. **API endpoint not responding**
   - **Check:** Network tab in browser dev tools
   - **Fix:** Verify server is running

**Debug steps:**

```bash
# Check if endpoint is accessible
curl -X GET https://yourdomain.com/api/subscription/portal \
  -H "Cookie: next-auth.session-token=..." \
  -v

# Check server logs
npm run dev  # Look for errors
```

---

### Issue: "No billing account found" Error

**Symptoms:**

- Click portal button
- Error: "No billing account found"
- Dealer has active subscription

**Possible causes:**

1. **No Stripe Customer ID in database**
   - **Check database:**
     ```sql
     SELECT stripeCustomerId FROM Dealer WHERE userId = '...';
     ```
   - **Fix:** Stripe customer may not have been created during checkout
   - **Resolution:** Re-run checkout flow or manually link customer

2. **Customer deleted in Stripe**
   - **Check:** Stripe Dashboard → Customers
   - **Fix:** Restore customer or create new one

3. **Wrong Stripe account**
   - **Check:** Environment variables point to correct Stripe account
   - **Fix:** Verify `STRIPE_SECRET_KEY` matches the account

**Debug steps:**

```typescript
// Add logging to portal endpoint
logger.info(
  {
    userId: session.user.id,
    dealerId: dealer?.id,
    stripeCustomerId: dealer?.stripeCustomerId,
  },
  'Portal access attempt'
);
```

---

### Issue: Portal Shows Wrong Dealer's Information

**Symptoms:**

- Dealer A logs in
- Portal shows Dealer B's billing info

**This should NEVER happen** - indicates serious security issue.

**Immediate actions:**

1. **Shut down portal access immediately**
2. **Check database queries:**

   ```typescript
   // Should be:
   const dealer = await prisma.dealer.findUnique({
     where: { userId: session.user.id }, // ← Must use session.user.id
   });
   ```

3. **Verify session authentication**
4. **Check for session fixation attacks**

**If this occurs, contact Stripe support immediately.**

---

### Issue: Portal Session Expires Immediately

**Symptoms:**

- Click portal button
- Redirected to portal
- Immediately see "Session expired" error

**Possible causes:**

1. **Server clock out of sync**
   - **Check:** Server time vs actual time
   - **Fix:** Sync server clock (NTP)

2. **Portal session creation failed**
   - **Check:** Server logs for Stripe API errors
   - **Fix:** Verify Stripe API keys are correct

3. **Stripe API version mismatch**
   - **Check:** API version in code vs Stripe Dashboard
   - **Fix:** Update API version in Stripe config

**Debug steps:**

```typescript
// Log portal session details
logger.info(
  {
    portalSessionId: portalSession.id,
    expiresAt: portalSession.expires_at,
    created: portalSession.created,
    customer: portalSession.customer,
  },
  'Portal session created'
);
```

---

### Issue: Webhooks Not Updating Status

**Symptoms:**

- Dealer cancels subscription in portal
- Database still shows "active"
- Dealer still has access

**Possible causes:**

1. **Webhook endpoint not receiving events**
   - **Check:** Stripe Dashboard → Developers → Webhooks
   - **Verify:** Webhook endpoint is active
   - **Fix:** Ensure webhook URL is correct

2. **Webhook signature verification failing**
   - **Check:** Server logs for signature errors
   - **Fix:** Verify `STRIPE_WEBHOOK_SECRET` is correct

3. **Webhook handler has errors**
   - **Check:** Server logs for errors in webhook processing
   - **Fix:** Debug webhook handler code

**Debug steps:**

```bash
# Local testing: Use Stripe CLI
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# View webhook logs in Stripe Dashboard
# Dashboard → Developers → Webhooks → [Your endpoint] → Logs
```

---

### Issue: Portal Not Styled with Brand

**Symptoms:**

- Portal has default Stripe branding
- Missing logo/colors

**Fix:**

1. Go to Stripe Dashboard → Settings → Branding
2. Upload logo
3. Set brand colors
4. Save changes
5. Portal updates immediately (no code changes needed)

---

### Issue: Rate Limit Errors

**Symptoms:**

- Click portal button multiple times
- Error: "Too many requests"
- Status code: 429

**This is expected behavior** if clicking rapidly.

**Rate limit:** Configured at Cloudflare edge

**Normal use:** 1-2 clicks per session

**If legitimate users hitting limit:**

1. Adjust rate limiting rules in Cloudflare Dashboard
2. Monitor Cloudflare analytics for abuse patterns

---

### Getting Help

#### Check Logs First

```bash
# Development
npm run dev  # Logs to console

# Production
# Check your logging service for:
# - 'Customer Portal session created'
# - 'Failed to create Customer Portal session'
# - Rate limit warnings
```

#### Stripe Dashboard

1. Go to Dashboard → Developers → Events
2. Search for events related to your customer
3. Check for API errors

#### Support Contacts

- **Stripe Support:** https://support.stripe.com
- **Internal Team:** Check docs/SUPPORT.md (if exists)

---

## Summary

### What You Built

✅ **Subscription information page** - Shows plan, status, features
✅ **Portal access button** - Creates secure session and redirects
✅ **API endpoints** - Handle authentication and portal creation
✅ **Security** - Multiple layers of authentication and validation
✅ **Self-service billing** - Dealers manage without support tickets

### What You Need to Do

**Required setup in Stripe Dashboard:**

1. Enable Customer Portal
2. Configure features (payment methods, invoices, billing address)
3. Set branding (logo, colors)
4. Configure business info (support email, privacy policy, terms)
5. Test in Test Mode
6. Configure in Live Mode

**Total setup time:** ~30 minutes

### What Dealers Get

✅ **Self-service** - Manage billing 24/7 without waiting for support
✅ **Secure** - PCI compliant, Stripe-hosted, industry standard
✅ **Professional** - Recognizable Stripe interface, trusted brand
✅ **Complete** - Payment methods, invoices, billing address, cancellation

### No Stripe Account Needed

Dealers authenticate with **your site** (Google/Apple OAuth), and you create a temporary "magic link" that grants them access to **only their billing info** on Stripe. No Stripe account, no login, no password required.

---

**Questions?** Refer to this document or contact the development team.

**Last Updated:** 2025-01-11
**Next Review:** After first production deployment
