54 KiB
Stripe Integration Testing Guide
Table of Contents
- Prerequisites
- Environment Setup
- Stripe Dashboard Configuration
- Test Card Numbers
- Testing Scenarios
- Webhook Testing
- Common Issues & Solutions
- 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)
- ✅ 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
- Log in to your Stripe Dashboard: https://dashboard.stripe.com
- Click on your profile icon in the top right corner
- Ensure Test Mode is enabled (you'll see "TEST DATA" banner at the top)
- If not enabled, toggle to "Switch to test data"
Step 2: Retrieve API Keys
-
Navigate to Developers → API keys
-
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
- Publishable key (starts with
-
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:
# 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:
# 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:
# 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:
# 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:
cd services/tenant
pip install -r requirements.txt
# This will install stripe==14.1.0
Frontend:
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
-
In Stripe Dashboard (Test Mode), go to Products → Add product
-
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)
- Product name:
-
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 asSTARTER_MONTHLY_PRICE_ID
-
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
-
Click Add product to create a new product
-
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
- Product name:
-
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
- Price:
-
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
- Price:
1.3 Create Enterprise Plan Product
-
Click Add product to create a new product
-
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
- Product name:
-
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
- Price:
-
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
- Price:
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:
- Creates the Stripe subscription with
trial_period_days=90 - 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
- Sets subscription status to
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
PILOT2025coupon code against your database - If valid, your backend passes
trial_period_days=90parameter 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:
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:
SELECT * FROM coupons WHERE code = 'PILOT2025';
Expected values:
code:PILOT2025discount_type:trial_extensiondiscount_value:90(days)max_redemptions:20active:truevalid_until: ~180 days from creation
If the coupon doesn't exist, it will be created automatically on application startup via the startup_seeder.py.
Environment Variables for Pilot Mode:
Frontend - Kubernetes ConfigMap:
These are already configured in infrastructure/kubernetes/base/configmap.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:
# 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. 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 below for detailed setup.
Quick start:
# 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 section for complete instructions on updating your configuration.
For Production or Public Testing:
-
Navigate to Developers → Webhooks in Stripe Dashboard
-
Click + Add endpoint
-
Endpoint URL:
- Production:
https://yourdomain.com/api/v1/stripe - Or use ngrok for testing:
https://your-ngrok-url.ngrok.io/api/v1/stripe
- Production:
-
Select events to listen to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failedcustomer.subscription.trial_will_endcoupon.created(for coupon tracking)coupon.deleted(for coupon tracking)promotion_code.created(if using promotion codes)
-
Click Add endpoint
-
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
.envfile asSTRIPE_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:
-
Start your Kubernetes environment:
Development (Kind/Colima + Tilt):
# Start Tilt (this starts all services) tilt up # Or if already running, just verify services are healthy kubectl get pods -n bakery-iaAccess the application:
- Tilt will provide the URLs (usually port-forwarded)
- Or use:
kubectl port-forward svc/frontend-service 3000:3000 -n bakery-ia
-
Navigate to registration page:
- Open browser:
http://localhost:3000/register(or your Tilt-provided URL)
- Open browser:
-
Fill in user details:
- Full Name:
John Doe - Email:
john.doe+test@example.com - Company:
Test Company - Password: Create a test password
- Full Name:
-
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
- Card Number:
-
Select a plan:
- Choose
Starter Plan(€49/month or €490/year)
- Choose
-
Do NOT apply coupon (this scenario tests without the PILOT2025 coupon)
-
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:
-
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
-
In your database:
SELECT * FROM subscriptions WHERE tenant_id = 'your-tenant-id';- Verify subscription record exists
- Status should be
active - Check
stripe_customer_idis populated - Verify
tier=starter - Verify
billing_cycle=monthlyoryearly - Check
current_period_startandcurrent_period_endare set
-
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:
-
Start your applications (same as Scenario 1)
-
Navigate to registration page:
- Open browser:
http://localhost:5173/register
- Open browser:
-
Fill in user details:
- Full Name:
Maria Garcia - Email:
maria.garcia+pilot@example.com - Company:
Panadería Piloto - Password: Create a test password
- Full Name:
-
Fill in payment details:
- Card Number:
4242 4242 4242 4242 - Expiry:
12/30 - CVC:
123 - Complete remaining billing details
- Card Number:
-
Select a plan:
- Choose
Professional Plan(€149/month or €1,490/year)
- Choose
-
Apply the PILOT2025 coupon:
- Enter coupon code:
PILOT2025 - Click "Apply" or "Validate"
- Verify coupon is accepted and shows "3 months free trial"
- Enter coupon code:
-
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:
-
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
-
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
-
In your database:
-- 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_endshould be 90 days in the future - Coupon redemption record should exist with:
coupon_code=PILOT2025redeemed_attimestamptenant_idmatches
- Subscription status should be
-
Test Coupon Re-use Prevention:
- Try registering another subscription with the same tenant
- Use coupon code
PILOT2025again - Expected: Should be rejected with error "Coupon already used by this tenant"
-
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:
-
Follow steps 1-3 from Scenario 1
-
Fill in payment details with 3DS card:
- Card Number:
4000 0025 0000 3155 - Expiry:
12/30 - CVC:
123 - Fill remaining details as before
- Card Number:
-
Submit the form
-
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:
-
Follow steps 1-3 from Scenario 1
-
Use a declined test card:
- Card Number:
4000 0000 0000 0002 - Fill remaining details as before
- Card Number:
-
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:
- Use card number:
4000 0000 0000 9995 - 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:
-
Create an active subscription (use Scenario 1)
-
Cancel the subscription:
- Method 1: Through your application UI (if implemented)
- Method 2: API call:
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_dateis set - ✅ User retains access until end of billing period
- ✅ Response includes days remaining
- ✅ Subscription cache invalidated
Verification:
-
Check database:
SELECT status, cancellation_effective_date, cancelled_at FROM subscriptions WHERE tenant_id = 'your-tenant-id'; -
Verify API response:
{ "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:
-
Cancel a subscription (use Scenario 5)
-
Reactivate the subscription:
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_atandcancellation_effective_datecleared - ✅ 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:
-
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
-
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:
-
In your database:
-- 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_redemptionsshows20(or your test limit) - Verify actual redemption count matches
- Verify
-
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
- Create a new coupon code (e.g.,
Scenario 8: Retrieve Invoices
Objective: Test invoice retrieval from Stripe.
Steps:
-
Create subscription with successful payment (Scenario 1)
-
Retrieve invoices:
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:
iddateamountcurrencystatusinvoice_pdfURLhosted_invoice_urlURL
Verification:
- Click on
hosted_invoice_urlto view invoice in browser - Download PDF from
invoice_pdfURL
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 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 completioncustomer.subscription.created- New subscription trackingcustomer.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 includes all necessary fields:
stripe_subscription_id- Links to Stripe subscriptionstripe_customer_id- Links to Stripe customerstatus- Tracks subscription statustrial_ends_at- Trial period tracking- Complete lifecycle management
Webhook Flow:
- Stripe sends event →
https://your-domain/webhooks/stripe - Signature verification with
STRIPE_WEBHOOK_SECRET - Event routing to appropriate handler
- Database updates (subscription status, dates, etc.)
- Cache invalidation for updated tenants
- 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):
# 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):
# 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:
# 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:
# 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:
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:
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:
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):
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:
# Look for signature errors
kubectl logs deployment/tenant-service -n bakery-ia | grep "Invalid webhook signature"
- Verify
STRIPE_WEBHOOK_SECRETin secrets.yaml matches Stripe Dashboard - Ensure webhook secret is properly base64 encoded
3. Database not updating:
# 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:
# 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
# Forward tenant service to localhost
kubectl port-forward svc/tenant-service 8001:8000 -n bakery-ia
Step 2: Use Stripe CLI to forward webhooks
# 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
# 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
# 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
# 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
kubectl port-forward svc/tenant-service 8001:8000 -n bakery-ia
Step 2: Start ngrok
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
# 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:
brew install stripe/stripe-cli/stripe
Windows: Download from: https://github.com/stripe/stripe-cli/releases
Linux:
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
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.
# 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:
-
Copy the webhook signing secret provided by
stripe listen -
Encode it for Kubernetes:
echo -n "whsec_abc123..." | base64 -
Update secrets.yaml:
# Edit infrastructure/kubernetes/base/secrets.yaml # Update the STRIPE_WEBHOOK_SECRET with the base64 value -
Apply to your cluster:
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:
# 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
ngrok http 8000
Output:
Forwarding https://abc123.ngrok.io -> http://localhost:8000
Step 3: Update Stripe Webhook Endpoint
- Go to Stripe Dashboard → Developers → Webhooks
- Click on your endpoint
- Update URL to:
https://abc123.ngrok.io/webhooks/stripe - 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:
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:
- Check internet connection (Stripe.js loads from CDN)
- Verify
VITE_STRIPE_PUBLISHABLE_KEYis set correctly - Check browser console for loading errors
- 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:
- Verify
STRIPE_WEBHOOK_SECRETmatches Stripe Dashboard - For Stripe CLI, use the secret from
stripe listenoutput - Ensure you're using the test mode secret, not live mode
- 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:
- Verify you're passing
paymentMethod.idto backend - Check that payment method is being attached to customer
- Ensure customer_id exists before creating subscription
- 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:
- Always check the mode indicator in Stripe Dashboard
- Test keys start with
pk_test_andsk_test_ - Live keys start with
pk_live_andsk_live_ - Never mix test and live keys
- Use separate databases for test and live environments
Issue 5: CORS Errors
Symptoms:
- Browser console shows CORS errors
- Requests to backend failing
Solutions:
- Ensure FastAPI CORS middleware is configured:
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:
- Check event type matches handler (case-sensitive)
- Verify database session is committed
- Check for exceptions in handler functions
- Review logs for specific error messages
- 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
elementsobject tocreatePaymentMethod({ 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
# 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
# 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:
-
Check Stripe Dashboard Logs:
- Developers → Logs
- View detailed request/response information
-
Review Application Logs:
- Check backend logs for detailed error messages
- Look for structured log output from
structlog
-
Test in Isolation:
- Test frontend separately
- Test backend API with cURL
- Verify webhook handling with Stripe CLI
-
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)
- Monthly: €49.00 EUR (Price ID:
- Create Professional Plan product with 2 prices:
- Monthly: €149.00 EUR (Price ID:
price_XXXXXX) - Yearly: €1,490.00 EUR (Price ID:
price_XXXXXX)
- Monthly: €149.00 EUR (Price ID:
- Create Enterprise Plan product with 2 prices:
- Monthly: €499.00 EUR (Price ID:
price_XXXXXX) - Yearly: €4,990.00 EUR (Price ID:
price_XXXXXX)
- Monthly: €499.00 EUR (Price ID:
- 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:
# 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:
# payment-secrets section
data:
STRIPE_SECRET_KEY: <base64_of_sk_test_key>
STRIPE_WEBHOOK_SECRET: <base64_of_whsec_key>
Encode your keys:
echo -n "sk_test_YOUR_KEY" | base64
echo -n "whsec_YOUR_SECRET" | base64
Apply to Kubernetes:
# 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:
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:
- Scenario 1: Registration without coupon (immediate billing)
- Scenario 1B: Registration with PILOT2025 coupon (90-day trial, €0 charge)
- Scenario 2: 3D Secure authentication
- Scenario 3: Declined payment
- Scenario 5: Subscription cancellation
- Scenario 7: Coupon redemption limit
- Webhook testing: Use Stripe CLI
5. Stripe CLI Setup (Recommended)
# 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:
- Coupon not created in database
- Frontend environment variable not set
- Coupon expired or inactive
Solution:
-- 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:
- Backend logs for trial_period_days parameter
- Verify coupon validation succeeded before subscription creation
- 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:
- Coupon validation failed silently
- trial_period_days not passed to Stripe
- Payment method attached before subscription (triggers charge)
Debug:
-- 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):
DELETE FROM coupon_redemptions WHERE tenant_id = 'test-tenant-id';