# Admin Status Change Implementation Plan

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

**Goal:** Enable admins to change dealer status via the quick actions menu, with Stripe subscription sync for suspend/cancel/resume operations.

**Architecture:** Click status badge or use quick actions menu → modal with status dropdown, danger warnings, reason selection → API endpoint validates transitions, updates database, syncs with Stripe (pause/cancel/resume subscription), and logs to AdminAction.

**Tech Stack:** Next.js 16 App Router, React 19, Prisma 7, Stripe API, TypeScript

**Dependencies:** This feature requires `feature/admin-subdomain-editor` to be merged first (provides QuickActionsMenu component).

---

## Task 1: Status Transition Validation Utility

**Files:**

- Create: `lib/status-transitions.ts`
- Create: `lib/__tests__/status-transitions.test.ts`

**Step 1: Write the failing test**

```typescript
// lib/__tests__/status-transitions.test.ts
import {
  canTransitionTo,
  getAvailableStatuses,
  isDangerousTransition,
  getTransitionWarning,
  DEALER_STATUSES,
} from '../status-transitions';

describe('status-transitions', () => {
  describe('canTransitionTo', () => {
    it('allows active → suspended', () => {
      expect(canTransitionTo('active', 'suspended')).toBe(true);
    });

    it('allows active → cancelled', () => {
      expect(canTransitionTo('active', 'cancelled')).toBe(true);
    });

    it('allows suspended → active (resume)', () => {
      expect(canTransitionTo('suspended', 'active')).toBe(true);
    });

    it('blocks cancelled → active (no reactivation)', () => {
      expect(canTransitionTo('cancelled', 'active')).toBe(false);
    });

    it('blocks any → payment_failed (system-only)', () => {
      expect(canTransitionTo('active', 'payment_failed')).toBe(false);
      expect(canTransitionTo('suspended', 'payment_failed')).toBe(false);
    });

    it('allows payment_failed → active (admin fix)', () => {
      expect(canTransitionTo('payment_failed', 'active')).toBe(true);
    });

    it('allows payment_failed → suspended', () => {
      expect(canTransitionTo('payment_failed', 'suspended')).toBe(true);
    });
  });

  describe('getAvailableStatuses', () => {
    it('returns correct options for active dealer', () => {
      const options = getAvailableStatuses('active');
      expect(options).toContain('suspended');
      expect(options).toContain('cancelled');
      expect(options).not.toContain('payment_failed');
      expect(options).not.toContain('active'); // Can't transition to self
    });

    it('returns correct options for suspended dealer', () => {
      const options = getAvailableStatuses('suspended');
      expect(options).toContain('active');
      expect(options).toContain('cancelled');
    });

    it('returns limited options for cancelled dealer', () => {
      const options = getAvailableStatuses('cancelled');
      expect(options).not.toContain('active'); // No reactivation
    });
  });

  describe('isDangerousTransition', () => {
    it('marks suspend as dangerous', () => {
      expect(isDangerousTransition('active', 'suspended')).toBe(true);
    });

    it('marks cancel as dangerous', () => {
      expect(isDangerousTransition('active', 'cancelled')).toBe(true);
    });

    it('does not mark resume as dangerous', () => {
      expect(isDangerousTransition('suspended', 'active')).toBe(false);
    });
  });

  describe('getTransitionWarning', () => {
    it('returns warning for suspend', () => {
      const warning = getTransitionWarning('active', 'suspended');
      expect(warning).toContain('disable');
    });

    it('returns warning for cancel', () => {
      const warning = getTransitionWarning('active', 'cancelled');
      expect(warning).toContain('terminate');
    });

    it('returns null for safe transitions', () => {
      expect(getTransitionWarning('suspended', 'active')).toBeNull();
    });
  });
});
```

**Step 2: Run test to verify it fails**

Run: `npx jest lib/__tests__/status-transitions.test.ts --no-coverage`
Expected: FAIL with "Cannot find module '../status-transitions'"

**Step 3: Write minimal implementation**

```typescript
// lib/status-transitions.ts

/**
 * Dealer status values and transition rules for admin status management.
 */

export const DEALER_STATUSES = [
  'active',
  'pending',
  'registration_pending',
  'suspended',
  'cancelled',
  'payment_failed',
] as const;

export type DealerStatus = (typeof DEALER_STATUSES)[number];

export interface StatusOption {
  value: DealerStatus;
  label: string;
  dangerous: boolean;
}

/**
 * Status transition rules:
 * - payment_failed: System-only, admins can transition FROM but not TO
 * - cancelled: Terminal state, no reactivation (would need new subscription)
 * - suspended: Can be resumed to active
 * - pending/registration_pending: Usually system-managed, but admin can override
 */
const BLOCKED_TRANSITIONS: Record<string, DealerStatus[]> = {
  // Can't set payment_failed manually - it's set by Stripe webhooks
  '*': ['payment_failed'],
  // Can't reactivate cancelled - subscription is gone
  cancelled: ['active'],
};

/**
 * Check if a status transition is allowed for admin.
 */
export function canTransitionTo(currentStatus: string, targetStatus: string): boolean {
  // Can't transition to self
  if (currentStatus === targetStatus) return false;

  // Check global blocks (applies to all statuses)
  const globalBlocked = BLOCKED_TRANSITIONS['*'] || [];
  if (globalBlocked.includes(targetStatus as DealerStatus)) {
    // Exception: payment_failed can be transitioned FROM
    return false;
  }

  // Check status-specific blocks
  const statusBlocked = BLOCKED_TRANSITIONS[currentStatus] || [];
  if (statusBlocked.includes(targetStatus as DealerStatus)) {
    return false;
  }

  return true;
}

/**
 * Get list of statuses an admin can transition to from current status.
 */
export function getAvailableStatuses(currentStatus: string): StatusOption[] {
  const options: StatusOption[] = [];

  const statusConfig: Record<DealerStatus, { label: string; dangerous: boolean }> = {
    active: { label: 'Active', dangerous: false },
    pending: { label: 'Pending', dangerous: false },
    registration_pending: { label: 'Registration Pending', dangerous: false },
    suspended: { label: 'Suspended', dangerous: true },
    cancelled: { label: 'Cancelled', dangerous: true },
    payment_failed: { label: 'Payment Failed', dangerous: true },
  };

  for (const status of DEALER_STATUSES) {
    if (canTransitionTo(currentStatus, status)) {
      const config = statusConfig[status];
      options.push({
        value: status,
        label: config.label,
        dangerous: config.dangerous,
      });
    }
  }

  return options;
}

/**
 * Check if a transition is considered dangerous (requires extra confirmation).
 */
export function isDangerousTransition(currentStatus: string, targetStatus: string): boolean {
  const dangerousTargets: DealerStatus[] = ['suspended', 'cancelled'];
  return dangerousTargets.includes(targetStatus as DealerStatus);
}

/**
 * Get warning message for a dangerous transition.
 */
export function getTransitionWarning(currentStatus: string, targetStatus: string): string | null {
  if (!isDangerousTransition(currentStatus, targetStatus)) {
    return null;
  }

  const warnings: Record<string, string> = {
    suspended: "This will disable the dealer's site and pause their subscription.",
    cancelled: 'This will permanently terminate their subscription. This cannot be undone.',
  };

  return warnings[targetStatus] || null;
}

/**
 * Reason options for status changes.
 */
export const STATUS_CHANGE_REASONS = [
  { value: 'policy_violation', label: 'Policy violation' },
  { value: 'non_payment', label: 'Non-payment' },
  { value: 'customer_request', label: 'Customer request' },
  { value: 'partner_promotion', label: 'Partner promotion' },
  { value: 'bug_correction', label: 'Bug correction' },
  { value: 'other', label: 'Other' },
] as const;

export type StatusChangeReason = (typeof STATUS_CHANGE_REASONS)[number]['value'];
```

**Step 4: Run test to verify it passes**

Run: `npx jest lib/__tests__/status-transitions.test.ts --no-coverage`
Expected: PASS

**Step 5: Commit**

```bash
git add lib/status-transitions.ts lib/__tests__/status-transitions.test.ts
git commit -m "feat(admin): add status transition validation utility"
```

---

## Task 2: Stripe Subscription Management Utility

**Files:**

- Create: `lib/stripe-subscription-admin.ts`

**Step 1: Create the Stripe admin utility**

```typescript
// lib/stripe-subscription-admin.ts
import { getStripeClient } from './stripe';
import { logger } from './logger';

/**
 * Admin utilities for managing Stripe subscriptions.
 * Used when admin changes dealer status.
 */

export interface StripeActionResult {
  success: boolean;
  error?: string;
  subscriptionId?: string;
  subscriptionStatus?: string;
}

/**
 * Pause a subscription (used when suspending a dealer).
 * Sets pause_collection to stop billing while keeping subscription active.
 */
export async function pauseSubscription(subscriptionId: string): Promise<StripeActionResult> {
  try {
    const stripe = getStripeClient();

    const subscription = await stripe.subscriptions.update(subscriptionId, {
      pause_collection: {
        behavior: 'mark_uncollectible',
      },
    });

    logger.info({ subscriptionId, status: subscription.status }, 'Paused Stripe subscription');

    return {
      success: true,
      subscriptionId: subscription.id,
      subscriptionStatus: subscription.status,
    };
  } catch (error) {
    logger.error({ err: error, subscriptionId }, 'Failed to pause subscription');
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Failed to pause subscription',
    };
  }
}

/**
 * Resume a paused subscription (used when reactivating a suspended dealer).
 * Removes pause_collection to resume billing.
 */
export async function resumeSubscription(subscriptionId: string): Promise<StripeActionResult> {
  try {
    const stripe = getStripeClient();

    // Setting pause_collection to null or empty object removes the pause
    const subscription = await stripe.subscriptions.update(subscriptionId, {
      pause_collection: null,
    });

    logger.info({ subscriptionId, status: subscription.status }, 'Resumed Stripe subscription');

    return {
      success: true,
      subscriptionId: subscription.id,
      subscriptionStatus: subscription.status,
    };
  } catch (error) {
    logger.error({ err: error, subscriptionId }, 'Failed to resume subscription');
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Failed to resume subscription',
    };
  }
}

/**
 * Cancel a subscription (used when cancelling a dealer).
 * This is a terminal action - subscription cannot be resumed.
 */
export async function cancelSubscription(
  subscriptionId: string,
  cancelImmediately: boolean = true
): Promise<StripeActionResult> {
  try {
    const stripe = getStripeClient();

    let subscription;

    if (cancelImmediately) {
      subscription = await stripe.subscriptions.cancel(subscriptionId);
    } else {
      // Cancel at end of billing period
      subscription = await stripe.subscriptions.update(subscriptionId, {
        cancel_at_period_end: true,
      });
    }

    logger.info(
      { subscriptionId, status: subscription.status, immediate: cancelImmediately },
      'Cancelled Stripe subscription'
    );

    return {
      success: true,
      subscriptionId: subscription.id,
      subscriptionStatus: subscription.status,
    };
  } catch (error) {
    logger.error({ err: error, subscriptionId }, 'Failed to cancel subscription');
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Failed to cancel subscription',
    };
  }
}

/**
 * Determine what Stripe action is needed for a status transition.
 */
export function getRequiredStripeAction(
  currentStatus: string,
  targetStatus: string
): 'pause' | 'resume' | 'cancel' | null {
  // Suspending → pause the subscription
  if (targetStatus === 'suspended' && currentStatus !== 'suspended') {
    return 'pause';
  }

  // Reactivating from suspended → resume the subscription
  if (targetStatus === 'active' && currentStatus === 'suspended') {
    return 'resume';
  }

  // Cancelling → cancel the subscription
  if (targetStatus === 'cancelled') {
    return 'cancel';
  }

  return null;
}
```

**Step 2: Verify TypeScript compiles**

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

**Step 3: Commit**

```bash
git add lib/stripe-subscription-admin.ts
git commit -m "feat(admin): add Stripe subscription pause/resume/cancel utilities"
```

---

## Task 3: API Endpoint - PATCH Status Route

**Files:**

- Create: `app/api/admin/dealers/[id]/status/route.ts`

**Step 1: Create the API route**

```typescript
// app/api/admin/dealers/[id]/status/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { requireAdmin, getRequestContext } from '@/lib/admin-auth';
import {
  canTransitionTo,
  isDangerousTransition,
  STATUS_CHANGE_REASONS,
  type StatusChangeReason,
} from '@/lib/status-transitions';
import {
  getRequiredStripeAction,
  pauseSubscription,
  resumeSubscription,
  cancelSubscription,
} from '@/lib/stripe-subscription-admin';
import { logger } from '@/lib/logger';

interface RouteParams {
  params: Promise<{ id: string }>;
}

export async function PATCH(request: NextRequest, { params }: RouteParams) {
  try {
    // Verify admin authentication
    const admin = await requireAdmin();
    if (!admin) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const { id: dealerId } = await params;
    const body = await request.json();
    const { status: targetStatus, reason, notes } = body;

    // Validate reason if provided
    if (reason && !STATUS_CHANGE_REASONS.some((r) => r.value === reason)) {
      return NextResponse.json({ error: 'Invalid reason' }, { status: 400 });
    }

    // Get current dealer
    const dealer = await prisma.dealer.findUnique({
      where: { id: dealerId },
      select: {
        id: true,
        status: true,
        stripeSubscriptionId: true,
        subdomain: true,
        businessName: true,
        user: {
          select: { email: true },
        },
      },
    });

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

    const currentStatus = dealer.status;

    // Validate transition is allowed
    if (!canTransitionTo(currentStatus, targetStatus)) {
      return NextResponse.json(
        {
          error: `Cannot transition from ${currentStatus} to ${targetStatus}`,
          currentStatus,
          targetStatus,
        },
        { status: 400 }
      );
    }

    // Require reason for dangerous transitions
    if (isDangerousTransition(currentStatus, targetStatus) && !reason) {
      return NextResponse.json(
        { error: 'Reason is required for this status change' },
        { status: 400 }
      );
    }

    // Handle Stripe subscription changes
    const stripeAction = getRequiredStripeAction(currentStatus, targetStatus);
    let stripeResult = null;

    if (stripeAction && dealer.stripeSubscriptionId) {
      switch (stripeAction) {
        case 'pause':
          stripeResult = await pauseSubscription(dealer.stripeSubscriptionId);
          break;
        case 'resume':
          stripeResult = await resumeSubscription(dealer.stripeSubscriptionId);
          break;
        case 'cancel':
          stripeResult = await cancelSubscription(dealer.stripeSubscriptionId);
          break;
      }

      if (stripeResult && !stripeResult.success) {
        logger.error(
          { dealerId, stripeAction, error: stripeResult.error },
          'Stripe action failed during status change'
        );
        return NextResponse.json(
          {
            error: `Failed to ${stripeAction} subscription: ${stripeResult.error}`,
          },
          { status: 500 }
        );
      }
    }

    // Update dealer status
    const updatedDealer = await prisma.dealer.update({
      where: { id: dealerId },
      data: {
        status: targetStatus,
        // Clear subscription ID if cancelled
        ...(targetStatus === 'cancelled' && { stripeSubscriptionId: null }),
      },
    });

    // Log admin action
    const { ipAddress, userAgent } = getRequestContext(request);
    await prisma.adminAction.create({
      data: {
        adminUserId: admin.id,
        action: 'change_status',
        targetType: 'dealer',
        targetId: dealerId,
        details: {
          previousStatus: currentStatus,
          newStatus: targetStatus,
          stripeAction,
          stripeResult: stripeResult
            ? { success: stripeResult.success, status: stripeResult.subscriptionStatus }
            : null,
        },
        reason: reason || null,
        ipAddress,
        userAgent,
      },
    });

    logger.info(
      {
        dealerId,
        adminId: admin.id,
        previousStatus: currentStatus,
        newStatus: targetStatus,
        stripeAction,
        reason,
      },
      'Admin changed dealer status'
    );

    return NextResponse.json({
      success: true,
      dealer: {
        id: updatedDealer.id,
        status: updatedDealer.status,
        previousStatus: currentStatus,
      },
      stripeAction,
    });
  } catch (error) {
    logger.error({ err: error }, 'Failed to change dealer status');
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
```

**Step 2: Verify TypeScript compiles**

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

**Step 3: Commit**

```bash
git add app/api/admin/dealers/[id]/status/route.ts
git commit -m "feat(admin): add PATCH endpoint for dealer status changes"
```

---

## Task 4: Status Change Modal Component

**Files:**

- Create: `components/admin/StatusChangeModal.tsx`
- Create: `components/admin/StatusChangeModal.module.css`

**Step 1: Create the modal component**

```typescript
// components/admin/StatusChangeModal.tsx
'use client';

import React, { useState, useEffect } from 'react';
import { Modal, ModalBody } from '@/components/ui/Modal';
import {
  getAvailableStatuses,
  getTransitionWarning,
  STATUS_CHANGE_REASONS,
  type StatusOption,
} from '@/lib/status-transitions';
import styles from './StatusChangeModal.module.css';

interface Dealer {
  id: string;
  status: string;
  subdomain: string | null;
  businessName: string | null;
}

interface StatusChangeModalProps {
  isOpen: boolean;
  onClose: () => void;
  dealer: Dealer | null;
  onSave: (dealerId: string, status: string, reason: string, notes?: string) => Promise<void>;
}

export function StatusChangeModal({
  isOpen,
  onClose,
  dealer,
  onSave,
}: StatusChangeModalProps) {
  const [selectedStatus, setSelectedStatus] = useState<string>('');
  const [reason, setReason] = useState<string>('');
  const [notes, setNotes] = useState<string>('');
  const [error, setError] = useState<string | null>(null);
  const [isSaving, setIsSaving] = useState(false);
  const [availableStatuses, setAvailableStatuses] = useState<StatusOption[]>([]);

  // Reset form when modal opens/closes or dealer changes
  useEffect(() => {
    if (isOpen && dealer) {
      setSelectedStatus('');
      setReason('');
      setNotes('');
      setError(null);
      setIsSaving(false);
      setAvailableStatuses(getAvailableStatuses(dealer.status));
    }
  }, [isOpen, dealer]);

  if (!dealer) return null;

  const warning = selectedStatus ? getTransitionWarning(dealer.status, selectedStatus) : null;
  const selectedOption = availableStatuses.find((s) => s.value === selectedStatus);
  const requiresReason = selectedOption?.dangerous ?? false;

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!selectedStatus) {
      setError('Please select a status');
      return;
    }

    if (requiresReason && !reason) {
      setError('Please select a reason for this change');
      return;
    }

    setIsSaving(true);
    setError(null);

    try {
      await onSave(dealer.id, selectedStatus, reason, notes || undefined);
      onClose();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to update status');
    } finally {
      setIsSaving(false);
    }
  };

  const getStatusBadgeClass = (status: string) => {
    const classes: Record<string, string> = {
      active: styles.statusActive,
      suspended: styles.statusSuspended,
      cancelled: styles.statusCancelled,
      pending: styles.statusPending,
      registration_pending: styles.statusPending,
      payment_failed: styles.statusPaymentFailed,
    };
    return classes[status] || styles.statusDefault;
  };

  const footer = (
    <div className={styles.footer}>
      <div className={styles.buttons}>
        <button
          type="button"
          className={styles.cancelButton}
          onClick={onClose}
          disabled={isSaving}
        >
          Cancel
        </button>
        <button
          type="submit"
          form="status-change-form"
          className={`${styles.confirmButton} ${selectedOption?.dangerous ? styles.dangerButton : ''}`}
          disabled={isSaving || !selectedStatus}
        >
          {isSaving ? 'Saving...' : selectedOption?.dangerous ? 'Confirm Change' : 'Update Status'}
        </button>
      </div>
      {warning && (
        <div className={styles.warning}>
          <svg
            className={styles.warningIcon}
            width="16"
            height="16"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
          >
            <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
            <line x1="12" y1="9" x2="12" y2="13" />
            <line x1="12" y1="17" x2="12.01" y2="17" />
          </svg>
          <span>{warning}</span>
        </div>
      )}
    </div>
  );

  return (
    <Modal isOpen={isOpen} onClose={onClose} title="Change Status" size="default" footer={footer}>
      <ModalBody>
        <form id="status-change-form" onSubmit={handleSubmit} className={styles.form}>
          {/* Dealer info */}
          <div className={styles.dealerInfo}>
            <span className={styles.dealerName}>
              {dealer.businessName || dealer.subdomain || 'Unnamed Dealer'}
            </span>
          </div>

          {/* Current status */}
          <div className={styles.currentStatus}>
            <span className={styles.label}>Current status:</span>
            <span className={`${styles.statusBadge} ${getStatusBadgeClass(dealer.status)}`}>
              {dealer.status.replace('_', ' ')}
            </span>
          </div>

          {/* Status selector */}
          <div className={styles.inputGroup}>
            <label htmlFor="status" className={styles.label}>
              New status:
            </label>
            <select
              id="status"
              value={selectedStatus}
              onChange={(e) => setSelectedStatus(e.target.value)}
              className={styles.select}
              disabled={isSaving}
            >
              <option value="">Select status...</option>
              {availableStatuses.map((option) => (
                <option key={option.value} value={option.value}>
                  {option.label}
                  {option.dangerous ? ' ⚠️' : ''}
                </option>
              ))}
            </select>
          </div>

          {/* Reason selector (required for dangerous transitions) */}
          <div className={styles.inputGroup}>
            <label htmlFor="reason" className={styles.label}>
              Reason{requiresReason ? ' *' : ''}:
            </label>
            <select
              id="reason"
              value={reason}
              onChange={(e) => setReason(e.target.value)}
              className={styles.select}
              disabled={isSaving}
              required={requiresReason}
            >
              <option value="">Select reason...</option>
              {STATUS_CHANGE_REASONS.map((r) => (
                <option key={r.value} value={r.value}>
                  {r.label}
                </option>
              ))}
            </select>
          </div>

          {/* Optional notes */}
          <div className={styles.inputGroup}>
            <label htmlFor="notes" className={styles.label}>
              Notes (optional):
            </label>
            <textarea
              id="notes"
              value={notes}
              onChange={(e) => setNotes(e.target.value)}
              className={styles.textarea}
              placeholder="Add any additional context..."
              rows={3}
              disabled={isSaving}
            />
          </div>

          {error && <p className={styles.error}>{error}</p>}
        </form>
      </ModalBody>
    </Modal>
  );
}
```

**Step 2: Create the styles**

```css
/* components/admin/StatusChangeModal.module.css */
.form {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.dealerInfo {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--color-border, rgba(148, 163, 184, 0.25));
}

.dealerName {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary, #f8fafc);
}

.currentStatus {
  display: flex;
  align-items: center;
  gap: 12px;
}

.label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text-secondary, rgba(203, 213, 225, 0.9));
}

.statusBadge {
  display: inline-flex;
  align-items: center;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: capitalize;
}

.statusActive {
  background: rgba(16, 185, 129, 0.2);
  color: #34d399;
}

.statusSuspended {
  background: rgba(239, 68, 68, 0.2);
  color: #f87171;
}

.statusCancelled {
  background: rgba(107, 114, 128, 0.2);
  color: #9ca3af;
}

.statusPending {
  background: rgba(245, 158, 11, 0.2);
  color: #fbbf24;
}

.statusPaymentFailed {
  background: rgba(239, 68, 68, 0.2);
  color: #f87171;
}

.statusDefault {
  background: rgba(148, 163, 184, 0.2);
  color: #94a3b8;
}

.inputGroup {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.select {
  padding: 10px 12px;
  background: rgba(15, 23, 42, 0.6);
  border: 1px solid var(--color-border, rgba(148, 163, 184, 0.25));
  border-radius: 8px;
  font-size: 0.9375rem;
  color: var(--color-text-primary, #f8fafc);
  cursor: pointer;
  transition: border-color 0.2s;
}

.select:focus {
  outline: none;
  border-color: var(--color-primary-accent, #38bdf8);
}

.select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.textarea {
  padding: 10px 12px;
  background: rgba(15, 23, 42, 0.6);
  border: 1px solid var(--color-border, rgba(148, 163, 184, 0.25));
  border-radius: 8px;
  font-size: 0.9375rem;
  color: var(--color-text-primary, #f8fafc);
  resize: vertical;
  min-height: 80px;
  font-family: inherit;
  transition: border-color 0.2s;
}

.textarea:focus {
  outline: none;
  border-color: var(--color-primary-accent, #38bdf8);
}

.textarea::placeholder {
  color: var(--color-text-tertiary, rgba(148, 163, 184, 0.85));
}

.textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.error {
  font-size: 0.8125rem;
  color: #ef4444;
  margin: 0;
}

.footer {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.buttons {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.cancelButton {
  padding: 10px 20px;
  background: transparent;
  border: 1px solid var(--color-border, rgba(148, 163, 184, 0.25));
  border-radius: 8px;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary, rgba(203, 213, 225, 0.9));
  cursor: pointer;
  transition: all 0.2s;
}

.cancelButton:hover:not(:disabled) {
  background: rgba(148, 163, 184, 0.15);
  border-color: rgba(148, 163, 184, 0.4);
}

.cancelButton:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.confirmButton {
  padding: 10px 20px;
  background: var(--color-primary-accent, #38bdf8);
  border: none;
  border-radius: 8px;
  font-size: 0.875rem;
  font-weight: 600;
  color: #0f172a;
  cursor: pointer;
  transition: all 0.2s;
}

.confirmButton:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px -8px rgba(14, 165, 233, 0.7);
}

.confirmButton:disabled {
  background: rgba(71, 85, 105, 0.4);
  color: rgba(226, 232, 240, 0.65);
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.dangerButton {
  background: #ef4444;
}

.dangerButton:hover:not(:disabled) {
  box-shadow: 0 8px 20px -8px rgba(239, 68, 68, 0.7);
}

.warning {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  background: rgba(245, 158, 11, 0.1);
  border: 1px solid rgba(245, 158, 11, 0.3);
  border-radius: 8px;
  font-size: 0.8125rem;
  color: #fbbf24;
}

.warningIcon {
  flex-shrink: 0;
  margin-top: 1px;
}
```

**Step 3: Verify TypeScript compiles**

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

**Step 4: Commit**

```bash
git add components/admin/StatusChangeModal.tsx components/admin/StatusChangeModal.module.css
git commit -m "feat(admin): add StatusChangeModal component"
```

---

## Task 5: Integrate into QuickActionsMenu and DealerTable

**Files:**

- Modify: `components/admin/QuickActionsMenu.tsx` (add Change Status action)
- Modify: `components/admin/DealerTable.tsx` (add modal and handler)

**Note:** This task assumes `feature/admin-subdomain-editor` has been merged. The implementing engineer should first merge/rebase to get the QuickActionsMenu component.

**Step 1: Add Change Status to QuickActionsMenu**

Add new prop and menu item to `components/admin/QuickActionsMenu.tsx`:

```typescript
// Add to interface
interface QuickActionsMenuProps {
  onStartSupport: () => void;
  onEditDNS: () => void;
  onChangeStatus: () => void;  // ADD THIS
  stripeCustomerId: string | null;
  isStartingSupport?: boolean;
}

// Add to component signature
export function QuickActionsMenu({
  onStartSupport,
  onEditDNS,
  onChangeStatus,  // ADD THIS
  stripeCustomerId,
  isStartingSupport,
}: QuickActionsMenuProps) {
```

Add menu item after "Edit DNS" button:

```typescript
          <button
            className={styles.menuItem}
            onClick={() => {
              onChangeStatus();
              setIsOpen(false);
            }}
            role="menuitem"
          >
            <svg
              className={styles.menuIcon}
              width="16"
              height="16"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
            >
              <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
              <path d="M12 6v6l4 2" />
            </svg>
            Change Status
          </button>
```

**Step 2: Add StatusChangeModal to DealerTable**

Add imports and state to `components/admin/DealerTable.tsx`:

```typescript
import { StatusChangeModal } from './StatusChangeModal';

// Add state in component
const [isStatusChangeOpen, setIsStatusChangeOpen] = useState(false);
const [statusChangeDealer, setStatusChangeDealer] = useState<Dealer | null>(null);

// Add handler
const handleChangeStatus = (dealer: Dealer) => {
  setStatusChangeDealer(dealer);
  setIsStatusChangeOpen(true);
};

// Add save handler
const handleSaveStatus = async (
  dealerId: string,
  status: string,
  reason: string,
  notes?: string
) => {
  const response = await fetch(`/api/admin/dealers/${dealerId}/status`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ status, reason, notes }),
  });

  if (!response.ok) {
    const data = await response.json();
    throw new Error(data.error || 'Failed to update status');
  }

  // Refresh the dealer list
  onDealerUpdated?.();
};
```

Update QuickActionsMenu usage:

```typescript
<QuickActionsMenu
  onStartSupport={() => onImpersonate(dealer.userId)}
  onEditDNS={() => handleEditDNS(dealer)}
  onChangeStatus={() => handleChangeStatus(dealer)}  // ADD THIS
  stripeCustomerId={dealer.stripeCustomerId}
  isStartingSupport={isImpersonating === dealer.userId}
/>
```

Add modal at end of component (after EditDNSModal):

```typescript
<StatusChangeModal
  isOpen={isStatusChangeOpen}
  onClose={() => {
    setIsStatusChangeOpen(false);
    setStatusChangeDealer(null);
  }}
  dealer={statusChangeDealer}
  onSave={handleSaveStatus}
/>
```

**Step 3: Verify and commit**

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

```bash
git add components/admin/QuickActionsMenu.tsx components/admin/DealerTable.tsx
git commit -m "feat(admin): integrate StatusChangeModal into DealerTable"
```

---

## Task 6: Manual Testing Checklist

**Step 1: Start the dev server**

```bash
npm run power-cycle
```

**Step 2: Test scenarios**

1. **View available statuses:**
   - Navigate to `/admin`
   - Click ⋮ → Change Status on an active dealer
   - Verify dropdown shows: Suspended, Cancelled (both with warning emoji)
   - Verify Active is NOT shown (can't transition to self)
   - Verify Payment Failed is NOT shown (system-only)

2. **Suspend a dealer:**
   - Select "Suspended" from dropdown
   - Verify warning appears below buttons
   - Select a reason (required for dangerous transitions)
   - Click Confirm → verify dealer status updates
   - Check Stripe dashboard → subscription should be paused

3. **Resume a suspended dealer:**
   - Click ⋮ → Change Status on suspended dealer
   - Verify "Active" is available
   - Select Active → no warning (not dangerous)
   - Reason is optional
   - Confirm → verify status updates
   - Check Stripe → subscription should be resumed

4. **Cancel a dealer:**
   - Click ⋮ → Change Status on active dealer
   - Select "Cancelled"
   - Verify strong warning about termination
   - Must select reason
   - Confirm → verify status is cancelled
   - Check Stripe → subscription should be cancelled

5. **Blocked transitions:**
   - On cancelled dealer, verify "Active" is NOT available
   - On any dealer, verify "Payment Failed" is NOT available

**Step 3: Commit any fixes**

```bash
git add -A
git commit -m "fix(admin): address issues found during manual testing"
```

---

## Task 7: Final Commit and Push

**Step 1: Run full test suite**

```bash
npm test
```

**Step 2: Run type check**

```bash
npx tsc --noEmit
```

**Step 3: Run build**

```bash
npm run build
```

**Step 4: Push branch**

```bash
git push -u origin feature/admin-status-change
```

---

## Summary

| Task | Description                                | Files                                        |
| ---- | ------------------------------------------ | -------------------------------------------- |
| 1    | Status transition validation               | `lib/status-transitions.ts`                  |
| 2    | Stripe subscription utilities              | `lib/stripe-subscription-admin.ts`           |
| 3    | PATCH API endpoint                         | `app/api/admin/dealers/[id]/status/route.ts` |
| 4    | StatusChangeModal component                | `components/admin/StatusChangeModal.tsx`     |
| 5    | QuickActionsMenu & DealerTable integration | `QuickActionsMenu.tsx`, `DealerTable.tsx`    |
| 6    | Manual testing                             | -                                            |
| 7    | Final verification and push                | -                                            |

---

## Future Enhancements (Out of Scope)

- Source tracking for webhook race condition protection (defer to tier management)
- Email notification when status changes (checkbox placeholder removed per requirements)
- Reactivation from cancelled (requires new checkout flow)
- Bulk status changes
