# 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: STRIPE_WEBHOOK_SECRET: ``` **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 -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: STRIPE_WEBHOOK_SECRET: ``` 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'; ```