# Account Upgrade Feature Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Enable dealers to upgrade their subscription tier from within the app via Stripe Checkout.

**Architecture:** Add upgrade section to existing subscription page with feature comparison table. Create new API endpoint that initiates Stripe Checkout for subscription upgrades. Connect existing tier handlers to webhook.

**Tech Stack:** Next.js 14 App Router, Stripe Checkout, React, CSS Modules

---

## Task 1: Create Upgrade Checkout API Endpoint

**Files:**

- Create: `app/api/checkout/upgrade/route.ts`

**Step 1: Create the upgrade endpoint**

```typescript
// app/api/checkout/upgrade/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { logger } from '@/lib/logger';
import { getStripeClient } from '@/lib/stripe';
import { getSiteUrl } from '@/lib/site-url';

export const dynamic = 'force-dynamic';

// Tier ranking for validation (higher = better plan)
const TIER_RANK: Record<string, number> = {
  starter: 1,
  growth: 2,
  enhanced: 3,
  professional: 4,
};

// Stripe Price ID mappings
const PLAN_PRICES: Record<string, string> = {
  starter: process.env.STRIPE_PRICE_STARTER!,
  growth: process.env.STRIPE_PRICE_GROWTH!,
  enhanced: process.env.STRIPE_PRICE_ENHANCED!,
  professional: process.env.STRIPE_PRICE_PROFESSIONAL!,
};

const VALID_UPGRADE_TIERS = ['growth', 'enhanced', 'professional'] as const;
type UpgradeTier = (typeof VALID_UPGRADE_TIERS)[number];

/**
 * POST /api/checkout/upgrade
 *
 * Creates a Stripe Checkout Session for upgrading an existing subscription.
 * Stripe handles proration automatically.
 *
 * Request body: { targetTier: 'growth' | 'enhanced' | 'professional' }
 * Returns: { checkoutUrl: string }
 */
export async function POST(request: NextRequest) {
  try {
    // Step 1: Verify authentication
    const session = await getServerSession(authOptions);
    if (!session?.user?.id) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // Step 2: Get dealer account
    const dealer = await prisma.dealer.findFirst({
      where: { userId: session.user.id },
      select: {
        id: true,
        stripeCustomerId: true,
        stripeSubscriptionId: true,
        subscriptionTier: true,
        status: true,
      },
    });

    if (!dealer) {
      return NextResponse.json({ error: 'Dealer account not found' }, { status: 404 });
    }

    if (!dealer.stripeCustomerId || !dealer.stripeSubscriptionId) {
      return NextResponse.json({ error: 'No active subscription found' }, { status: 400 });
    }

    if (dealer.status !== 'active') {
      return NextResponse.json({ error: 'Account must be active to upgrade' }, { status: 400 });
    }

    // Step 3: Parse and validate target tier
    const body = await request.json();
    const { targetTier } = body;

    if (!targetTier || !VALID_UPGRADE_TIERS.includes(targetTier as UpgradeTier)) {
      return NextResponse.json(
        { error: 'Invalid target tier. Must be: growth, enhanced, or professional' },
        { status: 400 }
      );
    }

    // Step 4: Validate upgrade direction (must be higher tier)
    const currentRank = TIER_RANK[dealer.subscriptionTier] || 0;
    const targetRank = TIER_RANK[targetTier];

    if (targetRank <= currentRank) {
      return NextResponse.json({ error: 'Can only upgrade to a higher tier' }, { status: 400 });
    }

    // Step 5: Get price ID for target tier
    const priceId = PLAN_PRICES[targetTier];
    if (!priceId) {
      logger.error({ targetTier }, 'Missing price ID for tier');
      return NextResponse.json({ error: 'Plan configuration error' }, { status: 500 });
    }

    // Step 6: Create Stripe Checkout Session for subscription upgrade
    const stripe = getStripeClient();
    const checkoutSession = await stripe.checkout.sessions.create({
      mode: 'subscription',
      customer: dealer.stripeCustomerId,
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      subscription_data: {
        metadata: {
          upgrade: 'true',
          previousTier: dealer.subscriptionTier,
          dealerId: dealer.id,
        },
      },
      metadata: {
        upgrade: 'true',
        previousTier: dealer.subscriptionTier,
        dealerId: dealer.id,
      },
      success_url: `${getSiteUrl().origin}/dashboard/subscription?upgraded=true`,
      cancel_url: `${getSiteUrl().origin}/dashboard/subscription?upgrade_cancelled=true`,
    });

    if (!checkoutSession.url) {
      throw new Error('Failed to create checkout session URL');
    }

    logger.info(
      {
        dealerId: dealer.id,
        fromTier: dealer.subscriptionTier,
        toTier: targetTier,
        sessionId: checkoutSession.id,
      },
      'Created upgrade checkout session'
    );

    return NextResponse.json({ checkoutUrl: checkoutSession.url });
  } catch (error) {
    logger.error({ err: error }, 'Error creating upgrade checkout session');
    return NextResponse.json({ error: 'Failed to create checkout session' }, { status: 500 });
  }
}
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/checkout/upgrade/route.ts
git commit -m "feat: add upgrade checkout API endpoint"
```

---

## Task 2: Create Plan Feature Data

**Files:**

- Create: `lib/plan-features.ts`

**Step 1: Create plan features data file**

```typescript
// lib/plan-features.ts

/**
 * Plan feature definitions for the upgrade comparison table.
 * Source: public/landing/index.html feature comparison section.
 */

export type TierName = 'starter' | 'growth' | 'enhanced' | 'professional';

export interface PlanFeature {
  name: string;
  starter: boolean;
  growth: boolean;
  enhanced: boolean;
  professional: boolean;
}

export const PLAN_FEATURES: PlanFeature[] = [
  {
    name: 'Set-up & Hosting',
    starter: true,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'AMSOIL Corporate Content Updates & Image Assets',
    starter: true,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Dealer Information Editor',
    starter: true,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Automatic transfer links to AMSOIL store',
    starter: true,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Core SEO & AI features',
    starter: true,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Lead generation form',
    starter: false,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Social Profile Linking',
    starter: false,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Option to Use a Custom Domain',
    starter: false,
    growth: true,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Featured content editor',
    starter: false,
    growth: false,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Advanced SEO & AI Enhancements',
    starter: false,
    growth: false,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Integrated Performance Dashboard',
    starter: false,
    growth: false,
    enhanced: true,
    professional: true,
  },
  {
    name: 'Optimized local content',
    starter: false,
    growth: false,
    enhanced: false,
    professional: true,
  },
  {
    name: 'Content section editor',
    starter: false,
    growth: false,
    enhanced: false,
    professional: true,
  },
  {
    name: 'Content publishing capabilities',
    starter: false,
    growth: false,
    enhanced: false,
    professional: true,
  },
];

export const PLAN_DISPLAY_NAMES: Record<TierName, string> = {
  starter: 'Starter',
  growth: 'Growth',
  enhanced: 'Enhanced',
  professional: 'Professional',
};

export const TIER_ORDER: TierName[] = ['starter', 'growth', 'enhanced', 'professional'];

/**
 * Get tier rank (1-4, higher = better)
 */
export function getTierRank(tier: string): number {
  const index = TIER_ORDER.indexOf(tier as TierName);
  return index >= 0 ? index + 1 : 0;
}

/**
 * Check if targetTier is higher than currentTier
 */
export function isUpgrade(currentTier: string, targetTier: string): boolean {
  return getTierRank(targetTier) > getTierRank(currentTier);
}
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add lib/plan-features.ts
git commit -m "feat: add plan features data for upgrade comparison"
```

---

## Task 3: Create Plan Comparison Component

**Files:**

- Create: `components/subscription/PlanComparison.tsx`
- Create: `components/subscription/PlanComparison.module.css`

**Step 1: Create the CSS module**

```css
/* components/subscription/PlanComparison.module.css */

.container {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.header {
  padding: var(--spacing-lg) var(--spacing-xl);
  border-bottom: 1px solid var(--color-border);
}

.title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-text-primary);
  margin: 0;
}

.tableWrapper {
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  min-width: 600px;
}

.table th,
.table td {
  padding: var(--spacing-md) var(--spacing-lg);
  text-align: center;
  border-bottom: 1px solid var(--color-border);
}

.table th:first-child,
.table td:first-child {
  text-align: left;
  font-weight: 500;
}

.table th {
  background: rgba(0, 0, 0, 0.1);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--color-text-primary);
}

.table tbody tr:last-child td {
  border-bottom: none;
}

.table tbody tr:hover {
  background: rgba(255, 255, 255, 0.02);
}

.featureName {
  color: var(--color-text-secondary);
  font-size: 0.9rem;
}

.check {
  color: #22c55e;
  font-size: 1.25rem;
}

.dash {
  color: var(--color-text-secondary);
  opacity: 0.5;
}

.currentTier {
  background: rgba(59, 130, 246, 0.1);
}

.currentBadge {
  display: inline-block;
  background: var(--color-primary-accent);
  color: white;
  font-size: 0.625rem;
  font-weight: 700;
  padding: 0.125rem 0.375rem;
  border-radius: var(--radius-sm);
  margin-left: var(--spacing-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.upgradeRow {
  background: rgba(0, 0, 0, 0.05);
}

.upgradeRow td {
  padding: var(--spacing-lg);
  border-bottom: none;
}

.upgradeButton {
  display: inline-block;
  background: var(--color-primary-accent);
  color: white;
  text-decoration: none;
  padding: var(--spacing-sm) var(--spacing-lg);
  border-radius: var(--radius-md);
  font-weight: 600;
  font-size: 0.875rem;
  border: none;
  cursor: pointer;
  transition: opacity var(--transition-base);
}

.upgradeButton:hover:not(:disabled) {
  opacity: 0.9;
}

.upgradeButton:disabled {
  background: var(--color-border);
  color: var(--color-text-secondary);
  cursor: not-allowed;
}

.grayed {
  opacity: 0.5;
}

.highestTierMessage {
  text-align: center;
  padding: var(--spacing-xl);
  color: var(--color-text-secondary);
}

.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
```

**Step 2: Create the component**

```typescript
// components/subscription/PlanComparison.tsx
'use client';

import { useState } from 'react';
import {
  PLAN_FEATURES,
  PLAN_DISPLAY_NAMES,
  TIER_ORDER,
  TierName,
  getTierRank,
} from '@/lib/plan-features';
import styles from './PlanComparison.module.css';

interface PlanComparisonProps {
  currentTier: string;
}

export function PlanComparison({ currentTier }: PlanComparisonProps) {
  const [loadingTier, setLoadingTier] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  const currentRank = getTierRank(currentTier);
  const isHighestTier = currentTier === 'professional';

  const handleUpgrade = async (targetTier: TierName) => {
    setLoadingTier(targetTier);
    setError(null);

    try {
      const response = await fetch('/api/checkout/upgrade', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ targetTier }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to create checkout session');
      }

      // Redirect to Stripe Checkout
      window.location.href = data.checkoutUrl;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Something went wrong');
      setLoadingTier(null);
    }
  };

  if (isHighestTier) {
    return (
      <div className={styles.container}>
        <div className={styles.header}>
          <h2 className={styles.title}>Plan Comparison</h2>
        </div>
        <div className={styles.highestTierMessage}>
          <p>You&apos;re on the Professional plan — our highest tier with all features included.</p>
        </div>
      </div>
    );
  }

  return (
    <div className={styles.container}>
      <div className={styles.header}>
        <h2 className={styles.title}>Upgrade Your Plan</h2>
      </div>

      {error && (
        <div style={{ padding: 'var(--spacing-md)', color: '#ef4444', textAlign: 'center' }}>
          {error}
        </div>
      )}

      <div className={styles.tableWrapper}>
        <table className={styles.table}>
          <thead>
            <tr>
              <th>Features</th>
              {TIER_ORDER.map((tier) => {
                const isCurrent = tier === currentTier;
                return (
                  <th
                    key={tier}
                    className={isCurrent ? styles.currentTier : undefined}
                  >
                    {PLAN_DISPLAY_NAMES[tier]}
                    {isCurrent && <span className={styles.currentBadge}>Current</span>}
                  </th>
                );
              })}
            </tr>
          </thead>
          <tbody>
            {PLAN_FEATURES.map((feature) => (
              <tr key={feature.name}>
                <td className={styles.featureName}>{feature.name}</td>
                {TIER_ORDER.map((tier) => {
                  const hasFeature = feature[tier];
                  const isCurrent = tier === currentTier;
                  const isLowerTier = getTierRank(tier) < currentRank;

                  return (
                    <td
                      key={tier}
                      className={`${isCurrent ? styles.currentTier : ''} ${isLowerTier ? styles.grayed : ''}`}
                    >
                      {hasFeature ? (
                        <span className={styles.check}>✓</span>
                      ) : (
                        <span className={styles.dash}>—</span>
                      )}
                    </td>
                  );
                })}
              </tr>
            ))}
            <tr className={styles.upgradeRow}>
              <td></td>
              {TIER_ORDER.map((tier) => {
                const isCurrent = tier === currentTier;
                const isLowerTier = getTierRank(tier) < currentRank;
                const canUpgrade = getTierRank(tier) > currentRank;
                const isLoading = loadingTier === tier;

                return (
                  <td
                    key={tier}
                    className={isCurrent ? styles.currentTier : undefined}
                  >
                    {canUpgrade ? (
                      <button
                        className={styles.upgradeButton}
                        onClick={() => handleUpgrade(tier)}
                        disabled={isLoading || loadingTier !== null}
                      >
                        {isLoading ? (
                          <span className={styles.loading}>
                            <span className={styles.spinner} />
                            Loading...
                          </span>
                        ) : (
                          `Upgrade to ${PLAN_DISPLAY_NAMES[tier]}`
                        )}
                      </button>
                    ) : isCurrent ? (
                      <span style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>
                        Current Plan
                      </span>
                    ) : isLowerTier ? (
                      <span className={styles.grayed} style={{ fontSize: '0.875rem' }}>
                        —
                      </span>
                    ) : null}
                  </td>
                );
              })}
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

export default PlanComparison;
```

**Step 3: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 4: Commit**

```bash
git add components/subscription/PlanComparison.tsx components/subscription/PlanComparison.module.css
git commit -m "feat: add PlanComparison component for upgrade UI"
```

---

## Task 4: Update Subscription Page

**Files:**

- Modify: `app/dashboard/subscription/page.tsx`

**Step 1: Add PlanComparison import and component**

Find the existing subscription page and add the upgrade section after the "Current Subscription" card.

Add import at top:

```typescript
import { PlanComparison } from '@/components/subscription/PlanComparison';
```

Add after the "Current Subscription Card" section (after line ~186, before "Billing Management Card"):

```typescript
        {/* Upgrade Plan Section */}
        <div className="mb-6">
          <PlanComparison currentTier={dealer.subscriptionTier} />
        </div>
```

**Step 2: Add success/cancelled message handling**

Add these imports at top:

```typescript
import { useSearchParams } from 'next/navigation';
```

Inside the component, after the useState declarations:

```typescript
const searchParams = useSearchParams();
const upgraded = searchParams.get('upgraded');
const upgradeCancelled = searchParams.get('upgrade_cancelled');
```

Add success message after the header, before "Current Subscription Card":

```typescript
        {upgraded && (
          <div className="bg-green-900/30 border border-green-600/50 text-green-200 px-4 py-3 rounded-lg mb-6">
            <strong>Upgrade successful!</strong> Your plan has been updated. It may take a moment for all features to activate.
          </div>
        )}

        {upgradeCancelled && (
          <div className="bg-yellow-900/30 border border-yellow-600/50 text-yellow-200 px-4 py-3 rounded-lg mb-6">
            Upgrade cancelled. No changes were made to your subscription.
          </div>
        )}
```

**Step 3: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 4: Commit**

```bash
git add app/dashboard/subscription/page.tsx
git commit -m "feat: add upgrade section to subscription page"
```

---

## Task 5: Connect Tier Handlers to Webhook

**Files:**

- Modify: `app/api/webhooks/stripe/route.ts`

**Step 1: Add tier handler imports**

At top of file, add:

```typescript
import { handleTierUpgrade, handleTierDowngrade } from '@/lib/tier-handlers';
```

**Step 2: Add tier rank helper**

After the imports, add:

```typescript
// Tier ranking for upgrade/downgrade detection
const TIER_RANK: Record<string, number> = {
  starter: 1,
  growth: 2,
  enhanced: 3,
  professional: 4,
};
```

**Step 3: Add tier handler calls in subscription.updated handler**

In the `customer.subscription.updated` case, after the `prisma.dealer.update()` call and before the logging, add:

```typescript
// Handle tier-specific changes (navigation items, features)
const oldTierRank = TIER_RANK[beforeState.tier] || 0;
const newTierRank = TIER_RANK[newTier] || 0;

if (newTierRank > oldTierRank) {
  await handleTierUpgrade(dealer.id, newTier);
  webhookLogger.info(
    { dealerId: dealer.id, fromTier: beforeState.tier, toTier: newTier },
    'Processed tier upgrade'
  );
} else if (newTierRank < oldTierRank) {
  await handleTierDowngrade(dealer.id, newTier);
  webhookLogger.info(
    { dealerId: dealer.id, fromTier: beforeState.tier, toTier: newTier },
    'Processed tier downgrade'
  );
}
```

**Step 4: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 5: Commit**

```bash
git add app/api/webhooks/stripe/route.ts
git commit -m "feat: connect tier handlers to subscription webhook"
```

---

## Task 6: Full Verification

**Step 1: Run TypeScript check**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 2: Run ESLint**

Run: `npx eslint app/api/checkout/upgrade lib/plan-features.ts components/subscription --max-warnings=0`
Expected: No errors or warnings

**Step 3: Run build**

Run: `npm run build`
Expected: Build succeeds

**Step 4: Final commit if any fixes were needed**

```bash
git add -A
git commit -m "fix: address linting and build issues"
```

---

## Summary

| Task | Files                                      | Description                       |
| ---- | ------------------------------------------ | --------------------------------- |
| 1    | `app/api/checkout/upgrade/route.ts`        | API endpoint for upgrade checkout |
| 2    | `lib/plan-features.ts`                     | Plan feature data                 |
| 3    | `components/subscription/PlanComparison.*` | Comparison table component        |
| 4    | `app/dashboard/subscription/page.tsx`      | Integrate upgrade section         |
| 5    | `app/api/webhooks/stripe/route.ts`         | Connect tier handlers             |
| 6    | -                                          | Full verification                 |

## Testing (Manual)

After implementation, test with dev auth:

1. Login as `test-starter@claude.dev`
2. Navigate to `/dashboard/subscription`
3. Verify comparison table shows with Starter as current
4. Click "Upgrade to Growth"
5. Verify redirect to Stripe Checkout
6. Complete test payment
7. Verify redirect back with success message
8. Verify tier updated in database
