> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with Stripe

> Change prices, add a plan, test billing locally, and recover from missed webhooks.

## Changing plan prices

No code changes required. Update prices entirely in Stripe:

1. Stripe Dashboard → **Products** → click on **Pro**
2. Click **Edit** on the pricing section
3. Stripe does not allow editing existing price amounts. Create a new price instead:
   * Click **Add another price**
   * Set the new amount and billing interval
   * Archive the old price
4. Copy the new Price ID → update `STRIPE_PRO_PRICE_ID` in your environment variables
5. Restart the API (or redeploy)

Existing subscribers stay on their current price. New subscribers use the new price.

## Adding a fourth plan

**Step 1:** Create the plan in Stripe Dashboard and copy the Price ID.

**Step 2:** Add a new environment variable: `STRIPE_ENTERPRISE_PRICE_ID=price_…`

**Step 3:** Add the plan to the `Plan` enum in Domain:

```csharp theme={null}
// src/Acme.Domain/Enums/Plan.cs
public enum Plan { Free, Pro, Business, Enterprise }
```

**Step 4:** Update `StripeService.GetPriceIdForPlan()`:

```csharp theme={null}
Plan.Enterprise => _config.EnterprisePriceId
    ?? throw new InvalidOperationException("Enterprise Price ID not configured"),
```

**Step 5:** Update the frontend `PLANS` constant:

```typescript theme={null}
// frontend/src/lib/constants.ts
export const PLANS = [
  { name: 'Free', price: 0, features: ['…'] },
  { name: 'Pro', price: 29, features: ['…'] },
  { name: 'Business', price: 79, features: ['…'] },
  { name: 'Enterprise', price: 199, features: ['…'] },
] as const;
```

## Testing the full billing flow locally

**Prerequisites:** Stripe CLI installed, `stripe listen` running, API started.

**Complete a test checkout:**

1. Log in to the frontend at `http://localhost:5173`
2. Navigate to **Billing** → click **Upgrade to Pro**
3. You are redirected to Stripe's test checkout
4. Use test card: `4242 4242 4242 4242`, any future expiry, any CVC
5. Click **Subscribe**
6. You are redirected back to `/billing?success=true`
7. The Billing page now shows the Pro plan

**Watch webhook events in the terminal:**

```
--> checkout.session.completed [evt_…]
<-- 200 POST http://localhost:5000/api/v1/webhooks/stripe [200ms]
```

**Verify the database:**

```bash theme={null}
make shell-db
SELECT plan, status, trial_ends_at FROM subscriptions WHERE tenant_id = '…';
\q
```

## Stripe test card numbers

| Scenario                    | Card number           |
| --------------------------- | --------------------- |
| Successful payment          | `4242 4242 4242 4242` |
| Card declined               | `4000 0000 0000 0002` |
| Requires 3DS authentication | `4000 0027 6000 3184` |
| Insufficient funds          | `4000 0000 0000 9995` |
| Expired card                | `4000 0000 0000 0069` |

Use any future expiry date and any 3-digit CVC for all test cards.

## Triggering specific webhook events manually

```bash theme={null}
# Test subscription deletion
stripe trigger customer.subscription.deleted

# Test payment failure
stripe trigger invoice.payment_failed

# Test payment success
stripe trigger invoice.payment_succeeded
```

<Note>
  `stripe trigger` uses Stripe's test fixture data. The `tenantId` in the metadata may not match a real tenant in your local database. Webhook handlers that look up a tenant by the metadata `tenantId` will log a warning and return `200` without updating local state. This is expected behavior — the trigger is useful for testing the webhook parsing and signature verification, not the full business logic.
</Note>

## Recovering from a missed webhook

The `SubscriptionSyncJob` runs every 2 hours and reconciles all tenant subscriptions with Stripe. If a webhook was missed, the sync job corrects local state within 2 hours automatically.

To correct immediately:

1. Go to the Hangfire dashboard: `http://localhost:5000/hangfire`
2. Click **Recurring Jobs**
3. Find `subscription-sync`
4. Click **Trigger now**

Or trigger it directly from a shell (requires admin access):

```bash theme={null}
# This endpoint triggers the sync job and returns immediately.
# The job runs asynchronously.
curl -X POST http://localhost:5000/api/v1/admin/jobs/subscription-sync \
  -H "Authorization: Bearer <admin-token>"
```
