# Stripe Integration Testing Guide

## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Environment Setup](#environment-setup)
3. [Stripe Dashboard Configuration](#stripe-dashboard-configuration)
4. [Test Card Numbers](#test-card-numbers)
5. [Testing Scenarios](#testing-scenarios)
6. [Webhook Testing](#webhook-testing)
7. [Common Issues & Solutions](#common-issues--solutions)
8. [Production Checklist](#production-checklist)


Flow Without 3DS Required

Step 1: POST /start-registration
├── Create Stripe Customer
├── Create SetupIntent with confirm=True
│   └── Stripe checks: "Does this card need 3DS?" → NO
│   └── SetupIntent status = 'succeeded' immediately
├── Store state: {customer_id, setup_intent_id, etc.}
└── Return: {requires_action: false, setup_intent_id}

Step 2: Frontend sees requires_action=false
└── Immediately calls /complete-registration (no 3DS popup)

Step 3: POST /complete-registration
├── Verify SetupIntent status = 'succeeded' ✓ (already succeeded)
├── Create Subscription with verified payment method
├── Create User, Tenant, etc.
└── Return: {user, tokens, subscription}
Flow With 3DS Required

Step 1: POST /start-registration
├── Create Stripe Customer
├── Create SetupIntent with confirm=True
│   └── Stripe checks: "Does this card need 3DS?" → YES
│   └── SetupIntent status = 'requires_action'
├── Store state: {customer_id, setup_intent_id, etc.}
└── Return: {requires_action: true, client_secret}

Step 2: Frontend sees requires_action=true
├── Shows 3DS popup: stripe.confirmCardSetup(client_secret)
├── User completes 3DS verification
└── Calls /complete-registration

Step 3: POST /complete-registration
├── Verify SetupIntent status = 'succeeded' ✓ (after 3DS)
├── Create Subscription with verified payment method
├── Create User, Tenant, etc.
└── Return: {user, tokens, subscription}



---

## Prerequisites

Before you begin testing, ensure you have:

- ✅ Stripe account created (sign up at [stripe.com](https://stripe.com))
- ✅ Node.js and Python environments set up
- ✅ Frontend application running (React + Vite)
- ✅ Backend API running (FastAPI)
- ✅ Database configured and accessible
- ✅ Redis instance running (for caching)


stripe listen --forward-to https://bakery-ia.local/api/v1/webhooks/stripe --skip-verify

---

## Environment Setup

### Step 1: Access Stripe Test Mode

1. Log in to your Stripe Dashboard: [https://dashboard.stripe.com](https://dashboard.stripe.com)
2. Click on your profile icon in the top right corner
3. Ensure **Test Mode** is enabled (you'll see "TEST DATA" banner at the top)
4. If not enabled, toggle to "Switch to test data"

### Step 2: Retrieve API Keys

1. Navigate to **Developers** → **API keys**
2. You'll see two types of keys:
   - **Publishable key** (starts with `pk_test_...`) - Used in frontend
   - **Secret key** (starts with `sk_test_...`) - Used in backend

3. Click "Reveal test key" for the Secret key and copy both keys

### Step 3: Configure Environment Variables

**IMPORTANT:** This project **runs exclusively in Kubernetes**:
- **Development:** Kind/Colima + Tilt for local K8s development
- **Production:** MicroK8s on Ubuntu VPS

All configuration is managed through Kubernetes ConfigMaps and Secrets.

#### Frontend - Kubernetes ConfigMap:

Update [infrastructure/kubernetes/base/configmap.yaml](infrastructure/kubernetes/base/configmap.yaml:374-378):

```yaml
# FRONTEND CONFIGURATION section (lines 374-378)
VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_your_actual_stripe_publishable_key_here"
VITE_PILOT_MODE_ENABLED: "true"
VITE_PILOT_COUPON_CODE: "PILOT2025"
VITE_PILOT_TRIAL_MONTHS: "3"
```

#### Backend - Kubernetes Secrets:

Update [infrastructure/kubernetes/base/secrets.yaml](infrastructure/kubernetes/base/secrets.yaml:142-150):

```yaml
# payment-secrets section (lines 142-150)
apiVersion: v1
kind: Secret
metadata:
  name: payment-secrets
  namespace: bakery-ia
type: Opaque
data:
  STRIPE_SECRET_KEY: <base64_encoded_sk_test_key>
  STRIPE_WEBHOOK_SECRET: <base64_encoded_whsec_key>
```

**Encode your Stripe keys:**
```bash
# Encode Stripe secret key
echo -n "sk_test_your_actual_key_here" | base64

# Encode webhook secret (obtained in next step)
echo -n "whsec_your_actual_secret_here" | base64
```

#### Apply Configuration Changes:

After updating the files, apply to your Kubernetes cluster:

```bash
# Development (Kind/Colima with Tilt)
# Tilt will automatically detect changes and reload

# Or manually apply
kubectl apply -f infrastructure/kubernetes/base/configmap.yaml
kubectl apply -f infrastructure/kubernetes/base/secrets.yaml

# Production (MicroK8s on VPS)
microk8s kubectl apply -f infrastructure/kubernetes/base/configmap.yaml
microk8s kubectl apply -f infrastructure/kubernetes/base/secrets.yaml

# Restart deployments to pick up new config
kubectl rollout restart deployment/frontend-deployment -n bakery-ia
kubectl rollout restart deployment/tenant-service -n bakery-ia
```

**Note:** The webhook secret will be obtained in the next step when setting up webhooks.

### Step 4: Install/Update Dependencies

#### Backend:
```bash
cd services/tenant
pip install -r requirements.txt
# This will install stripe==14.1.0
```

#### Frontend:
```bash
cd frontend
npm install
# Verifies @stripe/react-stripe-js and @stripe/stripe-js are installed
```

---

## Stripe Dashboard Configuration

### Step 1: Create Products and Prices

**Important:** Your application uses EUR currency and has specific pricing for the Spanish market.

#### 1.1 Create Starter Plan Product

1. In Stripe Dashboard (Test Mode), go to **Products** → **Add product**
2. Fill in product details:
   - **Product name:** `Starter Plan`
   - **Description:** `Plan básico para panaderías pequeñas - Includes basic forecasting, waste tracking, and supplier management`
   - **Statement descriptor:** `BAKERY-STARTER` (appears on customer's credit card statement)

3. **Create Monthly Price:**
   - Click **Add another price**
   - Price: `€49.00`
   - Billing period: `Monthly`
   - Currency: `EUR`
   - Price description: `Starter Plan - Monthly`
   - Click **Save price**
   - **⚠️ COPY THE PRICE ID** (starts with `price_...`) - Label it as `STARTER_MONTHLY_PRICE_ID`

4. **Create Yearly Price (with discount):**
   - Click **Add another price** on the same product
   - Price: `€490.00` (17% discount - equivalent to 2 months free)
   - Billing period: `Yearly`
   - Currency: `EUR`
   - Price description: `Starter Plan - Yearly (2 months free)`
   - Click **Save price**
   - **⚠️ COPY THE PRICE ID** - Label it as `STARTER_YEARLY_PRICE_ID`

#### 1.2 Create Professional Plan Product

1. Click **Add product** to create a new product
2. Fill in product details:
   - **Product name:** `Professional Plan`
   - **Description:** `Plan profesional para panaderías en crecimiento - Business analytics, enhanced AI (92% accurate), what-if scenarios, multi-location support`
   - **Statement descriptor:** `BAKERY-PRO`

3. **Create Monthly Price:**
   - Price: `€149.00`
   - Billing period: `Monthly`
   - Currency: `EUR`
   - Price description: `Professional Plan - Monthly`
   - **⚠️ COPY THE PRICE ID** - Label it as `PROFESSIONAL_MONTHLY_PRICE_ID`

4. **Create Yearly Price (with discount):**
   - Price: `€1,490.00` (17% discount - equivalent to 2 months free)
   - Billing period: `Yearly`
   - Currency: `EUR`
   - Price description: `Professional Plan - Yearly (2 months free)`
   - **⚠️ COPY THE PRICE ID** - Label it as `PROFESSIONAL_YEARLY_PRICE_ID`

#### 1.3 Create Enterprise Plan Product

1. Click **Add product** to create a new product
2. Fill in product details:
   - **Product name:** `Enterprise Plan`
   - **Description:** `Plan enterprise para grandes operaciones - Unlimited features, production distribution, centralized dashboard, white-label, SSO, dedicated support`
   - **Statement descriptor:** `BAKERY-ENTERPRISE`

3. **Create Monthly Price:**
   - Price: `€499.00`
   - Billing period: `Monthly`
   - Currency: `EUR`
   - Price description: `Enterprise Plan - Monthly`
   - **⚠️ COPY THE PRICE ID** - Label it as `ENTERPRISE_MONTHLY_PRICE_ID`

4. **Create Yearly Price (with discount):**
   - Price: `€4,990.00` (17% discount - equivalent to 2 months free)
   - Billing period: `Yearly`
   - Currency: `EUR`
   - Price description: `Enterprise Plan - Yearly (2 months free)`
   - **⚠️ COPY THE PRICE ID** - Label it as `ENTERPRISE_YEARLY_PRICE_ID`

#### 1.4 Trial Period Configuration

**IMPORTANT:** Your system does NOT use Stripe's default trial periods. Instead:
- All trial periods are managed through the **PILOT2025 coupon only**
- Do NOT configure default trials on products in Stripe
- The coupon provides a 90-day (3-month) trial extension
- Without the PILOT2025 coupon, subscriptions start with immediate billing

#### 1.5 Price ID Reference Sheet

Create a reference document with all your Price IDs:

```
STARTER_MONTHLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
STARTER_YEARLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
PROFESSIONAL_MONTHLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
PROFESSIONAL_YEARLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
ENTERPRISE_MONTHLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
ENTERPRISE_YEARLY_PRICE_ID=price_XXXXXXXXXXXXXXXX
```

⚠️ **You'll need these Price IDs when configuring your backend environment variables or updating subscription creation logic.**

### Step 2: Create the PILOT2025 Coupon (3 Months Free)

**CRITICAL:** This coupon provides a 90-day (3-month) trial period where customers pay €0.

#### How it Works:
Your application validates the `PILOT2025` coupon code and, when valid:
1. Creates the Stripe subscription with `trial_period_days=90`
2. Stripe automatically:
   - Sets subscription status to `trialing`
   - Charges **€0 for the first 90 days**
   - Schedules the first invoice for day 91
   - Automatically begins normal billing after trial ends

**IMPORTANT:** You do NOT need to create a coupon in Stripe Dashboard. The coupon is managed entirely in your application's database.

**How it works with Stripe:**
- Your application validates the `PILOT2025` coupon code against your database
- If valid, your backend passes `trial_period_days=90` parameter when creating the Stripe subscription
- Stripe doesn't know about the "PILOT2025" coupon itself - it only receives the trial duration
- Example API call to Stripe:
  ```python
  stripe.Subscription.create(
      customer=customer_id,
      items=[{"price": price_id}],
      trial_period_days=90,  # <-- This is what Stripe needs
      # No coupon parameter needed in Stripe
  )
  ```

#### Verify PILOT2025 Coupon in Your Database:

The PILOT2025 coupon should already exist in your database (created by the startup seeder). Verify it exists:

```sql
SELECT * FROM coupons WHERE code = 'PILOT2025';
```

**Expected values:**
- `code`: `PILOT2025`
- `discount_type`: `trial_extension`
- `discount_value`: `90` (days)
- `max_redemptions`: `20`
- `active`: `true`
- `valid_until`: ~180 days from creation

If the coupon doesn't exist, it will be created automatically on application startup via the [startup_seeder.py](services/tenant/app/jobs/startup_seeder.py).

#### Environment Variables for Pilot Mode:

**Frontend - Kubernetes ConfigMap:**

These are already configured in [infrastructure/kubernetes/base/configmap.yaml](infrastructure/kubernetes/base/configmap.yaml:374-378):

```yaml
# FRONTEND CONFIGURATION (lines 374-378)
VITE_PILOT_MODE_ENABLED: "true"
VITE_PILOT_COUPON_CODE: "PILOT2025"
VITE_PILOT_TRIAL_MONTHS: "3"
VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_your_stripe_publishable_key_here"
```

Update the `VITE_STRIPE_PUBLISHABLE_KEY` value with your actual test key from Stripe, then apply:

```bash
# Development (Tilt auto-reloads, but you can manually apply)
kubectl apply -f infrastructure/kubernetes/base/configmap.yaml

# Production
microk8s kubectl apply -f infrastructure/kubernetes/base/configmap.yaml
microk8s kubectl rollout restart deployment/frontend-deployment -n bakery-ia
```

**Backend Configuration:**

The backend coupon configuration is managed in code at [services/tenant/app/jobs/startup_seeder.py](services/tenant/app/jobs/startup_seeder.py). The PILOT2025 coupon with max_redemptions=20 is created automatically on application startup.

**Important Notes:**
- ✅ Coupon is validated in your application database, NOT in Stripe
- ✅ Works with ALL three tiers (Starter, Professional, Enterprise)
- ✅ Provides a 90-day (3-month) trial with **€0 charge** during trial
- ✅ Without this coupon, subscriptions start billing immediately
- ✅ After the 90-day trial ends, Stripe automatically bills the full monthly price
- ✅ Application tracks coupon usage to prevent double-redemption per tenant
- ✅ Limited to first 20 pilot customers

**Billing Example with PILOT2025:**
- Day 0: Customer subscribes to Professional Plan (€149/month) with PILOT2025
- Day 0-90: Customer pays **€0** (trialing status in Stripe)
- Day 91: Stripe automatically charges €149.00 and status becomes "active"
- Every 30 days after: €149.00 charged automatically

### Step 3: Configure Webhooks

**Important:** For local development, you'll use **Stripe CLI** instead of creating an endpoint in the Stripe Dashboard. The CLI automatically forwards webhook events to your local server.

#### For Local Development (Recommended):

**Use Stripe CLI** - See [Webhook Testing Section](#webhook-testing) below for detailed setup.

Quick start:
```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe  # macOS

# Login to Stripe
stripe login

# Forward webhooks to gateway
stripe listen --forward-to https://bakery-ia.local/api/v1/stripe
```

The CLI will provide a webhook signing secret. See the [Webhook Testing](#webhook-testing) section for complete instructions on updating your configuration.

#### For Production or Public Testing:

1. Navigate to **Developers** → **Webhooks** in Stripe Dashboard
2. Click **+ Add endpoint**

3. **Endpoint URL:**
   - Production: `https://yourdomain.com/api/v1/stripe`
   - Or use ngrok for testing: `https://your-ngrok-url.ngrok.io/api/v1/stripe`

4. **Select events to listen to:**
   - `checkout.session.completed`
   - `customer.subscription.created`
   - `customer.subscription.updated`
   - `customer.subscription.deleted`
   - `invoice.payment_succeeded`
   - `invoice.payment_failed`
   - `customer.subscription.trial_will_end`
   - `coupon.created` (for coupon tracking)
   - `coupon.deleted` (for coupon tracking)
   - `promotion_code.created` (if using promotion codes)

5. Click **Add endpoint**

6. **Copy the Webhook Signing Secret:**
   - Click on the newly created endpoint
   - Click **Reveal** next to "Signing secret"
   - Copy the secret (starts with `whsec_...`)
   - Add it to your backend `.env` file as `STRIPE_WEBHOOK_SECRET`

---

## Test Card Numbers

Stripe provides test card numbers to simulate different scenarios. **Never use real card details in test mode.**

### Basic Test Cards

| Scenario | Card Number | CVC | Expiry Date |
|----------|-------------|-----|-------------|
| **Successful payment** | `4242 4242 4242 4242` | Any 3 digits | Any future date |
| **Visa (debit)** | `4000 0566 5566 5556` | Any 3 digits | Any future date |
| **Mastercard** | `5555 5555 5555 4444` | Any 3 digits | Any future date |
| **American Express** | `3782 822463 10005` | Any 4 digits | Any future date |

### Authentication & Security

| Scenario | Card Number | Notes |
|----------|-------------|-------|
| **3D Secure authentication required** | `4000 0025 0000 3155` | Triggers authentication modal |
| **3D Secure 2 authentication** | `4000 0027 6000 3184` | Requires SCA authentication |

### Declined Cards

| Scenario | Card Number | Error Message |
|----------|-------------|---------------|
| **Generic decline** | `4000 0000 0000 0002` | Card declined |
| **Insufficient funds** | `4000 0000 0000 9995` | Insufficient funds |
| **Lost card** | `4000 0000 0000 9987` | Lost card |
| **Stolen card** | `4000 0000 0000 9979` | Stolen card |
| **Expired card** | `4000 0000 0000 0069` | Expired card |
| **Incorrect CVC** | `4000 0000 0000 0127` | Incorrect CVC |
| **Processing error** | `4000 0000 0000 0119` | Processing error |
| **Card declined (rate limit)** | `4000 0000 0000 9954` | Exceeds velocity limit |

### Additional Scenarios

| Scenario | Card Number | Notes |
|----------|-------------|-------|
| **Charge succeeds, then fails** | `4000 0000 0000 0341` | Attaches successfully but charge fails |
| **Dispute (fraudulent)** | `4000 0000 0000 0259` | Creates a fraudulent dispute |
| **Dispute (warning)** | `4000 0000 0000 2685` | Creates early fraud warning |

**Important Notes:**
- For **expiry date**: Use any future date (e.g., 12/30)
- For **CVC**: Use any 3-digit number (e.g., 123) or 4-digit for Amex (e.g., 1234)
- For **postal code**: Use any valid format (e.g., 12345)

---

## Testing Scenarios

### Scenario 1: Successful Registration with Payment (Starter Plan)

**Objective:** Test the complete registration flow with valid payment method for Starter Plan.

**Steps:**

1. **Start your Kubernetes environment:**

   **Development (Kind/Colima + Tilt):**
   ```bash
   # Start Tilt (this starts all services)
   tilt up

   # Or if already running, just verify services are healthy
   kubectl get pods -n bakery-ia
   ```

   **Access the application:**
   - Tilt will provide the URLs (usually port-forwarded)
   - Or use: `kubectl port-forward svc/frontend-service 3000:3000 -n bakery-ia`

2. **Navigate to registration page:**
   - Open browser: `http://localhost:3000/register` (or your Tilt-provided URL)

3. **Fill in user details:**
   - Full Name: `John Doe`
   - Email: `john.doe+test@example.com`
   - Company: `Test Company`
   - Password: Create a test password

4. **Fill in payment details:**
   - Card Number: `4242 4242 4242 4242`
   - Expiry: `12/30`
   - CVC: `123`
   - Cardholder Name: `John Doe`
   - Email: `john.doe+test@example.com`
   - Address: `123 Test Street`
   - City: `Test City`
   - State: `CA`
   - Postal Code: `12345`
   - Country: `US`

5. **Select a plan:**
   - Choose `Starter Plan` (€49/month or €490/year)

6. **Do NOT apply coupon** (this scenario tests without the PILOT2025 coupon)

7. **Submit the form**

**Expected Results:**
- ✅ Payment method created successfully
- ✅ User account created
- ✅ Subscription created in Stripe with immediate billing (no trial)
- ✅ Database records created
- ✅ User redirected to dashboard
- ✅ No console errors
- ✅ First invoice created immediately for €49.00

**Verification:**

1. **In Stripe Dashboard:**
   - Go to **Customers** → Find "John Doe"
   - Go to **Subscriptions** → See active subscription
   - Status should be `active` (no trial period without coupon)
   - Verify pricing: €49.00 EUR / month
   - Check that first invoice was paid immediately

2. **In your database:**
   ```sql
   SELECT * FROM subscriptions WHERE tenant_id = 'your-tenant-id';
   ```
   - Verify subscription record exists
   - Status should be `active`
   - Check `stripe_customer_id` is populated
   - Verify `tier` = `starter`
   - Verify `billing_cycle` = `monthly` or `yearly`
   - Check `current_period_start` and `current_period_end` are set

3. **Check application logs:**
   - Look for successful subscription creation messages
   - Verify no error logs
   - Check that subscription tier is cached properly

---

### Scenario 1B: Registration with PILOT2025 Coupon (3 Months Free)

**Objective:** Test the complete registration flow with the PILOT2025 coupon applied for 3-month free trial.

**Steps:**

1. **Start your applications** (same as Scenario 1)

2. **Navigate to registration page:**
   - Open browser: `http://localhost:5173/register`

3. **Fill in user details:**
   - Full Name: `Maria Garcia`
   - Email: `maria.garcia+pilot@example.com`
   - Company: `Panadería Piloto`
   - Password: Create a test password

4. **Fill in payment details:**
   - Card Number: `4242 4242 4242 4242`
   - Expiry: `12/30`
   - CVC: `123`
   - Complete remaining billing details

5. **Select a plan:**
   - Choose `Professional Plan` (€149/month or €1,490/year)

6. **Apply the PILOT2025 coupon:**
   - Enter coupon code: `PILOT2025`
   - Click "Apply" or "Validate"
   - Verify coupon is accepted and shows "3 months free trial"

7. **Submit the form**

**Expected Results:**
- ✅ Payment method created successfully
- ✅ User account created
- ✅ Subscription created in Stripe with 90-day trial period
- ✅ Database records created
- ✅ User redirected to dashboard
- ✅ No console errors
- ✅ Trial period: 90 days from today
- ✅ First invoice will be created after trial ends (90 days from now)
- ✅ Coupon redemption tracked in database

**Verification:**

1. **In Stripe Dashboard:**
   - Go to **Customers** → Find "Maria Garcia"
   - Go to **Subscriptions** → See active subscription
   - **Status should be `trialing`** (this is key!)
   - Verify pricing: €149.00 EUR / month (will charge this after trial)
   - **Check trial end date:** Should be **90 days** from creation (hover over trial end timestamp)
   - **Verify amount due now: €0.00** (no charge during trial)
   - Check upcoming invoice: Should be scheduled for 90 days from now for €149.00

2. **In Stripe Dashboard - Check Trial Status:**
   - Note: There is NO coupon to check in Stripe Dashboard (coupons are managed in your database)
   - Instead, verify the subscription has the correct trial period

3. **In your database:**
   ```sql
   -- Check subscription
   SELECT * FROM subscriptions WHERE tenant_id = 'maria-tenant-id';

   -- Check coupon redemption
   SELECT * FROM coupon_redemptions WHERE tenant_id = 'maria-tenant-id';
   ```
   - Subscription status should be `active`
   - Verify `tier` = `professional`
   - Check trial dates: `trial_end` should be 90 days in the future
   - Coupon redemption record should exist with:
     - `coupon_code` = `PILOT2025`
     - `redeemed_at` timestamp
     - `tenant_id` matches

4. **Test Coupon Re-use Prevention:**
   - Try registering another subscription with the same tenant
   - Use coupon code `PILOT2025` again
   - **Expected:** Should be rejected with error "Coupon already used by this tenant"

5. **Check application logs:**
   - Look for coupon validation messages
   - Verify coupon redemption was logged
   - Check subscription creation with trial extension
   - No errors related to Stripe or coupon processing

**Test Multiple Tiers:**
Repeat this scenario with all three plans to verify the coupon works universally:
- Starter Plan: €49/month → 90-day trial → €49/month after trial
- Professional Plan: €149/month → 90-day trial → €149/month after trial
- Enterprise Plan: €499/month → 90-day trial → €499/month after trial

---

### Scenario 2: Payment with 3D Secure Authentication

**Objective:** Test Strong Customer Authentication (SCA) flow.

**Steps:**

1. Follow steps 1-3 from Scenario 1

2. **Fill in payment details with 3DS card:**
   - Card Number: `4000 0025 0000 3155`
   - Expiry: `12/30`
   - CVC: `123`
   - Fill remaining details as before

3. **Submit the form**

4. **Complete authentication:**
   - Stripe will display an authentication modal
   - Click **"Complete"** (in test mode, no real auth needed)

**Expected Results:**
- ✅ Authentication modal appears
- ✅ After clicking "Complete", payment succeeds
- ✅ Subscription created successfully
- ✅ User redirected to dashboard

**Note:** This simulates European and other markets requiring SCA.

---

### Scenario 3: Declined Payment

**Objective:** Test error handling for declined cards.

**Steps:**

1. Follow steps 1-3 from Scenario 1

2. **Use a declined test card:**
   - Card Number: `4000 0000 0000 0002`
   - Fill remaining details as before

3. **Submit the form**

**Expected Results:**
- ❌ Payment fails with error message
- ✅ Error displayed to user: "Your card was declined"
- ✅ No customer created in Stripe
- ✅ No subscription created
- ✅ No database records created
- ✅ User remains on payment form
- ✅ Can retry with different card

**Verification:**
- Check Stripe Dashboard → Customers (should not see new customer)
- Check application logs for error handling
- Verify user-friendly error message displayed

---

### Scenario 4: Insufficient Funds

**Objective:** Test specific decline reason handling.

**Steps:**

1. Use card number: `4000 0000 0000 9995`
2. Follow same process as Scenario 3

**Expected Results:**
- ❌ Payment fails
- ✅ Error message: "Your card has insufficient funds"
- ✅ Proper error handling and logging

---

### Scenario 5: Subscription Cancellation

**Objective:** Test subscription cancellation flow.

**Steps:**

1. **Create an active subscription** (use Scenario 1)

2. **Cancel the subscription:**
   - Method 1: Through your application UI (if implemented)
   - Method 2: API call:
   ```bash
   curl -X POST http://localhost:8000/api/v1/subscriptions/cancel \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
     -d '{
       "tenant_id": "your-tenant-id",
       "reason": "Testing cancellation"
     }'
   ```

**Expected Results:**
- ✅ Subscription status changes to `pending_cancellation`
- ✅ `cancellation_effective_date` is set
- ✅ User retains access until end of billing period
- ✅ Response includes days remaining
- ✅ Subscription cache invalidated

**Verification:**
1. Check database:
   ```sql
   SELECT status, cancellation_effective_date, cancelled_at
   FROM subscriptions
   WHERE tenant_id = 'your-tenant-id';
   ```

2. Verify API response:
   ```json
   {
     "success": true,
     "message": "Subscription cancelled successfully...",
     "status": "pending_cancellation",
     "cancellation_effective_date": "2026-02-10T...",
     "days_remaining": 30
   }
   ```

---

### Scenario 6: Subscription Reactivation

**Objective:** Test reactivating a cancelled subscription.

**Steps:**

1. **Cancel a subscription** (use Scenario 5)

2. **Reactivate the subscription:**
   ```bash
   curl -X POST http://localhost:8000/api/v1/subscriptions/reactivate \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
     -d '{
       "tenant_id": "your-tenant-id",
       "plan": "starter"
     }'
   ```

**Expected Results:**
- ✅ Subscription status changes back to `active`
- ✅ `cancelled_at` and `cancellation_effective_date` cleared
- ✅ Next billing date set
- ✅ Subscription cache invalidated

---

### Scenario 7: PILOT2025 Coupon - Maximum Redemptions Reached

**Objective:** Test behavior when PILOT2025 coupon reaches its 20-customer limit.

**Preparation:**
- This test should be performed after 20 successful redemptions
- Or manually update coupon max redemptions in Stripe to a lower number for testing

**Steps:**

1. **Set up for testing:**
   - Option A: After 20 real redemptions
   - Option B: In Stripe Dashboard, edit PILOT2025 coupon and set "Maximum redemptions" to 1, then redeem it once

2. **Attempt to register with the exhausted coupon:**
   - Navigate to registration page
   - Fill in user details with a NEW user (e.g., `customer21@example.com`)
   - Fill in payment details
   - Select any plan
   - Enter coupon code: `PILOT2025`
   - Try to apply/validate the coupon

**Expected Results:**
- ❌ Coupon validation fails
- ✅ Error message displayed: "This coupon has reached its maximum redemption limit" or similar
- ✅ User cannot proceed with this coupon
- ✅ User can still register without the coupon (immediate billing)
- ✅ Application logs show coupon limit reached

**Verification:**

1. **In your database:**
   ```sql
   -- Check coupon redemption count
   SELECT code, current_redemptions, max_redemptions
   FROM coupons
   WHERE code = 'PILOT2025';

   -- Count actual redemptions
   SELECT COUNT(*) FROM coupon_redemptions WHERE coupon_code = 'PILOT2025';
   ```
   - Verify `current_redemptions` shows `20` (or your test limit)
   - Verify actual redemption count matches

2. **Test alternate registration path:**
   - Remove the coupon code
   - Complete registration without coupon
   - Verify subscription created successfully with immediate billing

**Important Notes:**
- Once the pilot program ends (20 customers), you can:
  - Create a new coupon code (e.g., `PILOT2026`) for future pilots
  - Increase the max redemptions if extending the program
  - Disable PILOT2025 in your application's environment variables

---

### Scenario 8: Retrieve Invoices

**Objective:** Test invoice retrieval from Stripe.

**Steps:**

1. **Create subscription with successful payment** (Scenario 1)

2. **Retrieve invoices:**
   ```bash
   curl -X GET http://localhost:8000/api/v1/subscriptions/{tenant_id}/invoices \
     -H "Authorization: Bearer YOUR_AUTH_TOKEN"
   ```

**Expected Results:**
- ✅ List of invoices returned
- ✅ Each invoice contains:
  - `id`
  - `date`
  - `amount`
  - `currency`
  - `status`
  - `invoice_pdf` URL
  - `hosted_invoice_url` URL

**Verification:**
- Click on `hosted_invoice_url` to view invoice in browser
- Download PDF from `invoice_pdf` URL

---

## Webhook Testing

Webhooks are critical for handling asynchronous events from Stripe. Test them thoroughly.

### Webhook Implementation Status

**✅ Your webhook implementation is COMPLETE and ready for production use.**

The webhook endpoint is implemented in [services/tenant/app/api/webhooks.py](services/tenant/app/api/webhooks.py:29-118) with:

**Implemented Features:**
- ✅ Proper signature verification (lines 52-61)
- ✅ Direct endpoint at `/webhooks/stripe` (bypasses gateway)
- ✅ All critical event handlers:
  - `checkout.session.completed` - New checkout completion
  - `customer.subscription.created` - New subscription tracking
  - `customer.subscription.updated` - Status changes, plan updates (lines 162-205)
  - `customer.subscription.deleted` - Cancellation handling (lines 207-240)
  - `invoice.payment_succeeded` - Payment success, activation (lines 242-266)
  - `invoice.payment_failed` - Payment failure, past_due status (lines 268-297)
  - `customer.subscription.trial_will_end` - Trial ending notification
- ✅ Database updates with proper async handling
- ✅ Subscription cache invalidation (lines 192-200, 226-235)
- ✅ Structured logging with `structlog`

**Database Model Support:**
The [Subscription model](services/tenant/app/models/tenants.py:149-185) includes all necessary fields:
- `stripe_subscription_id` - Links to Stripe subscription
- `stripe_customer_id` - Links to Stripe customer
- `status` - Tracks subscription status
- `trial_ends_at` - Trial period tracking
- Complete lifecycle management

**Webhook Flow:**
1. Stripe sends event → `https://your-domain/webhooks/stripe`
2. Signature verification with `STRIPE_WEBHOOK_SECRET`
3. Event routing to appropriate handler
4. Database updates (subscription status, dates, etc.)
5. Cache invalidation for updated tenants
6. Success/failure response to Stripe

### Checking Tenant Service Logs

Since webhooks go directly to the tenant service (bypassing the gateway), you need to monitor tenant service logs to verify webhook processing.

#### Kubernetes Log Commands

**Real-time logs (Development with Kind/Colima):**
```bash
# Follow tenant service logs in real-time
kubectl logs -f deployment/tenant-service -n bakery-ia

# Filter for Stripe-related events only
kubectl logs -f deployment/tenant-service -n bakery-ia | grep -i stripe

# Filter for webhook events specifically
kubectl logs -f deployment/tenant-service -n bakery-ia | grep "webhook"
```

**Real-time logs (Production with MicroK8s):**
```bash
# Follow tenant service logs in real-time
microk8s kubectl logs -f deployment/tenant-service -n bakery-ia

# Filter for Stripe events
microk8s kubectl logs -f deployment/tenant-service -n bakery-ia | grep -i stripe

# Filter for webhook events
microk8s kubectl logs -f deployment/tenant-service -n bakery-ia | grep "webhook"
```

**Historical logs:**
```bash
# View last hour of logs
kubectl logs --since=1h deployment/tenant-service -n bakery-ia

# View last 100 log lines
kubectl logs --tail=100 deployment/tenant-service -n bakery-ia

# View logs for specific pod
kubectl get pods -n bakery-ia  # Find pod name
kubectl logs <pod-name> -n bakery-ia
```

**Search logs for specific events:**
```bash
# Find subscription update events
kubectl logs deployment/tenant-service -n bakery-ia | grep "subscription.updated"

# Find payment processing
kubectl logs deployment/tenant-service -n bakery-ia | grep "payment"

# Find errors
kubectl logs deployment/tenant-service -n bakery-ia | grep -i error
```

#### Expected Log Output

When webhooks are processed successfully, you should see logs like:

**Successful webhook processing:**
```json
INFO Processing Stripe webhook event event_type=customer.subscription.updated event_id=evt_xxx
INFO Subscription updated in database subscription_id=sub_xxx tenant_id=tenant-uuid-xxx
```

**Payment succeeded:**
```json
INFO Processing invoice.payment_succeeded invoice_id=in_xxx subscription_id=sub_xxx
INFO Payment succeeded, subscription activated subscription_id=sub_xxx tenant_id=tenant-uuid-xxx
```

**Payment failed:**
```json
ERROR Processing invoice.payment_failed invoice_id=in_xxx subscription_id=sub_xxx customer_id=cus_xxx
WARNING Payment failed, subscription marked past_due subscription_id=sub_xxx tenant_id=tenant-uuid-xxx
```

**Signature verification errors (indicates webhook secret mismatch):**
```json
ERROR Invalid webhook signature error=...
```

#### Troubleshooting Webhook Issues via Logs

**1. Webhook not reaching service:**
- Check if any logs appear when Stripe sends webhook
- Verify network connectivity to tenant service
- Confirm webhook URL is correct in Stripe Dashboard

**2. Signature verification failing:**
```bash
# Look for signature errors
kubectl logs deployment/tenant-service -n bakery-ia | grep "Invalid webhook signature"
```
- Verify `STRIPE_WEBHOOK_SECRET` in secrets.yaml matches Stripe Dashboard
- Ensure webhook secret is properly base64 encoded

**3. Database not updating:**
```bash
# Check for database-related errors
kubectl logs deployment/tenant-service -n bakery-ia | grep -i "database\|commit\|rollback"
```
- Verify database connection is healthy
- Check for transaction commit errors
- Look for subscription not found errors

**4. Cache invalidation failing:**
```bash
# Check for cache-related errors
kubectl logs deployment/tenant-service -n bakery-ia | grep -i "cache\|redis"
```
- Verify Redis connection is healthy
- Check if cache service is running

### Testing Webhooks in Kubernetes Environment

You have two options for testing webhooks when running in Kubernetes:

#### Option 1: Using Stripe CLI with Port-Forward (Recommended for Local Dev)

This approach works with your Kind/Colima + Tilt setup:

**Step 1: Port-forward the tenant service**
```bash
# Forward tenant service to localhost
kubectl port-forward svc/tenant-service 8001:8000 -n bakery-ia
```

**Step 2: Use Stripe CLI to forward webhooks**
```bash
# In a new terminal, forward webhooks to the port-forwarded service
stripe listen --forward-to http://localhost:8001/webhooks/stripe
```

**Step 3: Copy the webhook secret**
```bash
# The stripe listen command will output a webhook secret like:
# > Ready! Your webhook signing secret is whsec_abc123...

# Encode it for Kubernetes secrets
echo -n "whsec_abc123..." | base64
```

**Step 4: Update secrets and restart**
```bash
# Update the STRIPE_WEBHOOK_SECRET in secrets.yaml with the base64 value
# Then apply:
kubectl apply -f infrastructure/kubernetes/base/secrets.yaml
kubectl rollout restart deployment/tenant-service -n bakery-ia
```

**Step 5: Test webhook events**
```bash
# In another terminal, trigger test events
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_succeeded

# Watch tenant service logs
kubectl logs -f deployment/tenant-service -n bakery-ia
```

#### Option 2: Using ngrok for Public URL (Works for Dev and Prod Testing)

This approach exposes your local Kubernetes cluster (or VPS) to the internet:

**For Development (Kind/Colima):**

**Step 1: Port-forward tenant service**
```bash
kubectl port-forward svc/tenant-service 8001:8000 -n bakery-ia
```

**Step 2: Start ngrok**
```bash
ngrok http 8001
```

**Step 3: Configure Stripe webhook**
- Copy the ngrok HTTPS URL (e.g., `https://abc123.ngrok.io`)
- Go to Stripe Dashboard → Developers → Webhooks
- Add endpoint: `https://abc123.ngrok.io/webhooks/stripe`
- Copy the webhook signing secret
- Update secrets.yaml and restart tenant service

**For Production (MicroK8s on VPS):**

If your VPS has a public IP, you can configure webhooks directly:

**Step 1: Ensure tenant service is accessible**
```bash
# Check if tenant service is exposed externally
microk8s kubectl get svc tenant-service -n bakery-ia

# If using NodePort or LoadBalancer, get the public endpoint
# If using Ingress, ensure DNS points to your VPS
```

**Step 2: Configure Stripe webhook**
- Use your public URL: `https://your-domain.com/webhooks/stripe`
- Or IP: `https://your-vps-ip:port/webhooks/stripe`
- Add endpoint in Stripe Dashboard
- Copy webhook signing secret
- Update secrets.yaml and apply to production cluster

### Option 1: Using Stripe CLI (Recommended for Local Development)

#### Step 1: Install Stripe CLI

**macOS:**
```bash
brew install stripe/stripe-cli/stripe
```

**Windows:**
Download from: https://github.com/stripe/stripe-cli/releases

**Linux:**
```bash
wget https://github.com/stripe/stripe-cli/releases/latest/download/stripe_linux_x86_64.tar.gz
tar -xvf stripe_linux_x86_64.tar.gz
sudo mv stripe /usr/local/bin/
```

#### Step 2: Login to Stripe

```bash
stripe login
```

This opens a browser to authorize the CLI.

#### Step 3: Forward Webhooks to Local Server

**For Development with Stripe CLI:**

The Stripe CLI creates a secure tunnel to forward webhook events from Stripe's servers to your local development environment.

```bash
# Forward webhook events to your gateway (which proxies to tenant service)
stripe listen --forward-to https://bakery-ia.local/api/v1/stripe
```

**Expected Output:**
```
> Ready! Your webhook signing secret is whsec_abc123... (^C to quit)
```

**Important - Update Your Configuration:**

1. **Copy the webhook signing secret** provided by `stripe listen`

2. **Encode it for Kubernetes:**
   ```bash
   echo -n "whsec_abc123..." | base64
   ```

3. **Update secrets.yaml:**
   ```bash
   # Edit infrastructure/kubernetes/base/secrets.yaml
   # Update the STRIPE_WEBHOOK_SECRET with the base64 value
   ```

4. **Apply to your cluster:**
   ```bash
   kubectl apply -f infrastructure/kubernetes/base/secrets.yaml
   kubectl rollout restart deployment/tenant-service -n bakery-ia
   ```

**Note:** The webhook secret from `stripe listen` is temporary and only works while the CLI is running. Each time you restart `stripe listen`, you'll get a new webhook secret.

#### Step 4: Trigger Test Events

Open a new terminal and run:

```bash
# Test subscription created
stripe trigger customer.subscription.created

# Test payment succeeded
stripe trigger invoice.payment_succeeded

# Test payment failed
stripe trigger invoice.payment_failed

# Test subscription updated
stripe trigger customer.subscription.updated

# Test subscription deleted
stripe trigger customer.subscription.deleted

# Test trial ending
stripe trigger customer.subscription.trial_will_end
```

#### Step 5: Verify Webhook Processing

**Check your application logs for:**
- ✅ "Processing Stripe webhook event"
- ✅ Event type logged
- ✅ Database updates (check subscription status)
- ✅ No signature verification errors

**Example log output:**
```
INFO Processing Stripe webhook event event_type=customer.subscription.updated
INFO Subscription updated in database subscription_id=sub_123 tenant_id=tenant-id
```

### Option 2: Using ngrok (For Public URL Testing)

#### Step 1: Install ngrok

Download from: https://ngrok.com/download

#### Step 2: Start ngrok

```bash
ngrok http 8000
```

**Output:**
```
Forwarding https://abc123.ngrok.io -> http://localhost:8000
```

#### Step 3: Update Stripe Webhook Endpoint

1. Go to Stripe Dashboard → Developers → Webhooks
2. Click on your endpoint
3. Update URL to: `https://abc123.ngrok.io/webhooks/stripe`
4. Save changes

#### Step 4: Test by Creating Real Events

Create a test subscription through your app, and webhooks will be sent to your ngrok URL.

### Option 3: Testing Webhook Handlers Directly

You can also test webhook handlers by sending test payloads:

```bash
curl -X POST http://localhost:8000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "stripe-signature: test-signature" \
  -d @webhook-test-payload.json
```

**Note:** This will fail signature verification unless you disable it temporarily for testing.

---

## Common Issues & Solutions

### Issue 1: "Stripe.js has not loaded correctly"

**Symptoms:**
- Error when submitting payment form
- Console error about Stripe not being loaded

**Solutions:**
1. Check internet connection (Stripe.js loads from CDN)
2. Verify `VITE_STRIPE_PUBLISHABLE_KEY` is set correctly
3. Check browser console for loading errors
4. Ensure no ad blockers blocking Stripe.js

### Issue 2: "Invalid signature" on Webhook

**Symptoms:**
- Webhook endpoint returns 400 error
- Log shows "Invalid webhook signature"

**Solutions:**
1. Verify `STRIPE_WEBHOOK_SECRET` matches Stripe Dashboard
2. For Stripe CLI, use the secret from `stripe listen` output
3. Ensure you're using the test mode secret, not live mode
4. Check that you're not modifying the request body before verification

### Issue 3: Payment Method Not Attaching

**Symptoms:**
- PaymentMethod created but subscription fails
- Error about payment method not found

**Solutions:**
1. Verify you're passing `paymentMethod.id` to backend
2. Check that payment method is being attached to customer
3. Ensure customer_id exists before creating subscription
4. Review backend logs for detailed error messages

### Issue 4: Test Mode vs Live Mode Confusion

**Symptoms:**
- Keys not working
- Data not appearing in dashboard

**Solutions:**
1. **Always check the mode indicator** in Stripe Dashboard
2. Test keys start with `pk_test_` and `sk_test_`
3. Live keys start with `pk_live_` and `sk_live_`
4. Never mix test and live keys
5. Use separate databases for test and live environments

### Issue 5: CORS Errors

**Symptoms:**
- Browser console shows CORS errors
- Requests to backend failing

**Solutions:**
1. Ensure FastAPI CORS middleware is configured:
   ```python
   from fastapi.middleware.cors import CORSMiddleware

   app.add_middleware(
       CORSMiddleware,
       allow_origins=["http://localhost:5173"],
       allow_credentials=True,
       allow_methods=["*"],
       allow_headers=["*"],
   )
   ```

### Issue 6: Webhook Events Not Processing

**Symptoms:**
- Webhooks received but database not updating
- Events logged but handlers not executing

**Solutions:**
1. Check event type matches handler (case-sensitive)
2. Verify database session is committed
3. Check for exceptions in handler functions
4. Review logs for specific error messages
5. Ensure subscription exists in database before update

### Issue 7: Card Element vs Payment Element Confusion

**Symptoms:**
- TypeError when calling `stripe.createPaymentMethod()`
- Elements not rendering correctly

**Solutions:**
- Use `PaymentElement` (modern, recommended)
- Call `elements.submit()` before creating payment method
- Pass `elements` object to `createPaymentMethod({ elements })`
- **Our implementation now uses the correct PaymentElement API**

---

## Production Checklist

Before going live with Stripe payments:

### Security

- [ ] All API keys stored in environment variables (never in code)
- [ ] Webhook signature verification enabled and working
- [ ] HTTPS enabled on all endpoints
- [ ] Rate limiting implemented on payment endpoints
- [ ] Input validation on all payment-related forms
- [ ] SQL injection prevention (using parameterized queries)
- [ ] XSS protection enabled
- [ ] CSRF tokens implemented where needed

### Stripe Configuration

- [ ] Live mode API keys obtained from Stripe Dashboard
- [ ] Live mode webhook endpoints configured
- [ ] Webhook signing secret updated for live mode
- [ ] Products and prices created in live mode
- [ ] Business information completed in Stripe Dashboard
- [ ] Bank account added for payouts
- [ ] Tax settings configured (if applicable)
- [ ] Stripe account activated and verified

### Application Configuration

- [ ] Environment variables updated for production
- [ ] Database migrations run on production database
- [ ] Redis cache configured and accessible
- [ ] Error monitoring/logging configured (e.g., Sentry)
- [ ] Payment failure notifications set up
- [ ] Trial ending notifications configured
- [ ] Invoice email delivery tested

### Testing

- [ ] All test scenarios passed (see above)
- [ ] Webhook handling verified for all event types
- [ ] 3D Secure authentication tested
- [ ] Subscription lifecycle tested (create, update, cancel, reactivate)
- [ ] Error handling tested for all failure scenarios
- [ ] Invoice retrieval tested
- [ ] Load testing completed
- [ ] Security audit performed

### Monitoring

- [ ] Stripe Dashboard monitoring set up
- [ ] Application logs reviewed regularly
- [ ] Webhook delivery monitoring configured
- [ ] Payment success/failure metrics tracked
- [ ] Alert thresholds configured
- [ ] Failed payment retry logic implemented

### Compliance

- [ ] Terms of Service updated to mention subscriptions
- [ ] Privacy Policy updated for payment data handling
- [ ] GDPR compliance verified (if applicable)
- [ ] PCI compliance requirements reviewed
- [ ] Customer data retention policy defined
- [ ] Refund policy documented

### Documentation

- [ ] API documentation updated
- [ ] Internal team trained on Stripe integration
- [ ] Customer support documentation created
- [ ] Troubleshooting guide prepared
- [ ] Subscription management procedures documented

---

## Quick Reference Commands

### Stripe CLI Commands

```bash
# Login to Stripe
stripe login

# Listen for webhooks (local development)
stripe listen --forward-to localhost:8000/webhooks/stripe

# Trigger test events
stripe trigger customer.subscription.created
stripe trigger invoice.payment_succeeded
stripe trigger invoice.payment_failed

# View recent events
stripe events list

# Get specific event
stripe events retrieve evt_abc123

# Test webhook endpoint
stripe webhooks test --endpoint-secret whsec_abc123
```

### Testing Shortcuts

```bash
# Start backend (from project root)
cd services/tenant && uvicorn app.main:app --reload --port 8000

# Start frontend (from project root)
cd frontend && npm run dev

# Update backend dependencies
cd services/tenant && pip install -r requirements.txt

# Run database migrations (if using Alembic)
cd services/tenant && alembic upgrade head
```

---

## Additional Resources

- **Stripe Documentation:** https://stripe.com/docs
- **Stripe API Reference:** https://stripe.com/docs/api
- **Stripe Testing Guide:** https://stripe.com/docs/testing
- **Stripe Webhooks Guide:** https://stripe.com/docs/webhooks
- **Stripe CLI Documentation:** https://stripe.com/docs/stripe-cli
- **React Stripe.js Docs:** https://stripe.com/docs/stripe-js/react
- **Stripe Support:** https://support.stripe.com

---

## Support

If you encounter issues not covered in this guide:

1. **Check Stripe Dashboard Logs:**
   - Developers → Logs
   - View detailed request/response information

2. **Review Application Logs:**
   - Check backend logs for detailed error messages
   - Look for structured log output from `structlog`

3. **Test in Isolation:**
   - Test frontend separately
   - Test backend API with cURL
   - Verify webhook handling with Stripe CLI

4. **Contact Stripe Support:**
   - Live chat available in Stripe Dashboard
   - Email support: support@stripe.com
   - Community forum: https://stripe.com/community

---

**Last Updated:** January 2026
**Stripe Library Versions:**
- Frontend: `@stripe/stripe-js@4.0.0`, `@stripe/react-stripe-js@3.0.0`
- Backend: `stripe@14.1.0`

---

## Appendix A: Quick Setup Checklist for Bakery IA

Use this checklist when setting up your Stripe test environment:

### 1. Stripe Dashboard Setup

- [ ] Create Stripe account and enable Test Mode
- [ ] Create **Starter Plan** product with 2 prices:
  - [ ] Monthly: €49.00 EUR (Price ID: `price_XXXXXX`)
  - [ ] Yearly: €490.00 EUR (Price ID: `price_XXXXXX`)
- [ ] Create **Professional Plan** product with 2 prices:
  - [ ] Monthly: €149.00 EUR (Price ID: `price_XXXXXX`)
  - [ ] Yearly: €1,490.00 EUR (Price ID: `price_XXXXXX`)
- [ ] Create **Enterprise Plan** product with 2 prices:
  - [ ] Monthly: €499.00 EUR (Price ID: `price_XXXXXX`)
  - [ ] Yearly: €4,990.00 EUR (Price ID: `price_XXXXXX`)
- [ ] Do NOT configure default trial periods on products
- [ ] Copy all 6 Price IDs for configuration
- [ ] Retrieve Stripe API keys (publishable and secret)
- [ ] Configure webhook endpoint (use Stripe CLI for local testing)
- [ ] Get webhook signing secret

### 2. Environment Configuration

**Note:** This project runs exclusively in Kubernetes (Kind/Colima + Tilt for dev, MicroK8s for prod).

**Frontend - Kubernetes ConfigMap:**

Update [infrastructure/kubernetes/base/configmap.yaml](infrastructure/kubernetes/base/configmap.yaml:374-378):
```yaml
# FRONTEND CONFIGURATION section
VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_XXXXXXXXXXXXXXXX"
VITE_PILOT_MODE_ENABLED: "true"
VITE_PILOT_COUPON_CODE: "PILOT2025"
VITE_PILOT_TRIAL_MONTHS: "3"
```

**Backend - Kubernetes Secrets:**

Update [infrastructure/kubernetes/base/secrets.yaml](infrastructure/kubernetes/base/secrets.yaml:142-150):
```yaml
# payment-secrets section
data:
  STRIPE_SECRET_KEY: <base64_of_sk_test_key>
  STRIPE_WEBHOOK_SECRET: <base64_of_whsec_key>
```

Encode your keys:
```bash
echo -n "sk_test_YOUR_KEY" | base64
echo -n "whsec_YOUR_SECRET" | base64
```

**Apply to Kubernetes:**
```bash
# Development (Kind/Colima with Tilt - auto-reloads)
kubectl apply -f infrastructure/kubernetes/base/configmap.yaml
kubectl apply -f infrastructure/kubernetes/base/secrets.yaml

# Production (MicroK8s)
microk8s kubectl apply -f infrastructure/kubernetes/base/configmap.yaml
microk8s kubectl apply -f infrastructure/kubernetes/base/secrets.yaml
microk8s kubectl rollout restart deployment/frontend-deployment -n bakery-ia
microk8s kubectl rollout restart deployment/tenant-service -n bakery-ia
```

### 3. Database Verification

- [ ] Verify PILOT2025 coupon exists in database:
  ```sql
  SELECT * FROM coupons WHERE code = 'PILOT2025';
  ```
- [ ] Confirm coupon has max_redemptions = 20 and active = true

### 4. Test Scenarios Priority Order

Run these tests in order:

1. **Scenario 1:** Registration without coupon (immediate billing)
2. **Scenario 1B:** Registration with PILOT2025 coupon (90-day trial, €0 charge)
3. **Scenario 2:** 3D Secure authentication
4. **Scenario 3:** Declined payment
5. **Scenario 5:** Subscription cancellation
6. **Scenario 7:** Coupon redemption limit
8. **Webhook testing:** Use Stripe CLI

### 5. Stripe CLI Setup (Recommended)

```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe  # macOS

# Login
stripe login

# Forward webhooks to local backend
stripe listen --forward-to localhost:8000/webhooks/stripe

# Copy the webhook secret (whsec_...) to your backend .env
```

---

## Appendix B: Pricing Summary

### Monthly Pricing (EUR)

| Tier | Monthly | Yearly | Yearly Savings | Trial (with PILOT2025) |
|------|---------|--------|----------------|------------------------|
| **Starter** | €49 | €490 | €98 (17%) | 90 days €0 |
| **Professional** | €149 | €1,490 | €298 (17%) | 90 days €0 |
| **Enterprise** | €499 | €4,990 | €998 (17%) | 90 days €0 |

### PILOT2025 Coupon Details

- **Code:** `PILOT2025`
- **Type:** Trial extension (90 days)
- **Discount:** 100% off for first 3 months (€0 charge)
- **Applies to:** All tiers
- **Max redemptions:** 20 customers
- **Valid period:** 180 days from creation
- **Managed in:** Application database (NOT Stripe)

### Billing Timeline Example

**Professional Plan with PILOT2025:**

| Day | Event | Charge |
|-----|-------|--------|
| 0 | Subscription created | €0 |
| 1-90 | Trial period (trialing status) | €0 |
| 91 | Trial ends, first invoice | €149 |
| 121 | Second invoice (30 days later) | €149 |
| 151 | Third invoice | €149 |
| ... | Monthly recurring | €149 |

**Professional Plan without coupon:**

| Day | Event | Charge |
|-----|-------|--------|
| 0 | Subscription created | €149 |
| 30 | First renewal | €149 |
| 60 | Second renewal | €149 |
| ... | Monthly recurring | €149 |

### Test Card Quick Reference

| Purpose | Card Number | Result |
|---------|-------------|--------|
| **Success** | `4242 4242 4242 4242` | Payment succeeds |
| **3D Secure** | `4000 0025 0000 3155` | Requires authentication |
| **Declined** | `4000 0000 0000 0002` | Card declined |
| **Insufficient Funds** | `4000 0000 0000 9995` | Insufficient funds |

**Always use:**
- Expiry: Any future date (e.g., `12/30`)
- CVC: Any 3 digits (e.g., `123`)
- ZIP: Any valid format (e.g., `12345`)

---

## Appendix C: Troubleshooting PILOT2025 Coupon

### Issue: Coupon code not recognized

**Possible causes:**
1. Coupon not created in database
2. Frontend environment variable not set
3. Coupon expired or inactive

**Solution:**
```sql
-- Check if coupon exists
SELECT * FROM coupons WHERE code = 'PILOT2025';

-- If not found, manually insert (or restart application to trigger seeder)
INSERT INTO coupons (code, discount_type, discount_value, max_redemptions, active, valid_from, valid_until)
VALUES ('PILOT2025', 'trial_extension', 90, 20, true, NOW(), NOW() + INTERVAL '180 days');
```

### Issue: Trial not applying in Stripe

**Check:**
1. Backend logs for trial_period_days parameter
2. Verify coupon validation succeeded before subscription creation
3. Check subscription creation params in Stripe Dashboard logs

**Expected log output:**
```
INFO: Coupon PILOT2025 validated successfully
INFO: Creating subscription with trial_period_days=90
INFO: Stripe subscription created with status=trialing
```

### Issue: Customer charged immediately despite coupon

**Possible causes:**
1. Coupon validation failed silently
2. trial_period_days not passed to Stripe
3. Payment method attached before subscription (triggers charge)

**Debug:**
```sql
-- Check if coupon was redeemed
SELECT * FROM coupon_redemptions WHERE tenant_id = 'your-tenant-id';

-- Check subscription trial dates
SELECT stripe_subscription_id, status, trial_ends_at
FROM subscriptions WHERE tenant_id = 'your-tenant-id';
```

### Issue: Coupon already redeemed error

**This is expected behavior.** Each tenant can only use PILOT2025 once. To test multiple times:
- Create new tenant accounts with different emails
- Or manually delete redemption record from database (test only):
  ```sql
  DELETE FROM coupon_redemptions WHERE tenant_id = 'test-tenant-id';
  ```
