1716 lines
52 KiB
Markdown
1716 lines
52 KiB
Markdown
# 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)
|
|
|
|
---
|
|
|
|
## 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)
|
|
|
|
---
|
|
|
|
## 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';
|
|
```
|