Sequence Diagram: Stripe Checkout Flow
Purpose: Complete subscription creation workflow from plan selection through Stripe Checkout and license activation.
Actors:
- User (customer purchasing CODITECT)
- Web Dashboard (React frontend)
- License API (Django on GKE)
- Stripe (payment processing)
- PostgreSQL (license storage)
Flow: Stripe Checkout Session → Payment → Webhook → License Creation
Mermaid Sequence Diagram
Step-by-Step Breakdown
1. Create Checkout Session (Steps 1-7)
Client-side: Initiate checkout:
// Client-side: React component for plan selection
import React from 'react';
import axios from 'axios';
interface PlanCardProps {
tier: 'pro' | 'team' | 'enterprise';
cycle: 'monthly' | 'annual';
price: number;
features: string[];
}
const PlanCard: React.FC<PlanCardProps> = ({ tier, cycle, price, features }) => {
const handleUpgrade = async () => {
try {
// Get JWT token from auth context
const token = localStorage.getItem('jwt_token');
// Create checkout session
const response = await axios.post(
'https://api.coditect.ai/api/v1/billing/checkout',
{
tier,
cycle
},
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
// Redirect to Stripe Checkout
window.location.href = response.data.checkout_url;
} catch (error) {
if (error.response?.status === 400) {
alert('You already have an active subscription');
} else {
alert('Checkout failed. Please try again.');
}
}
};
return (
<div className="plan-card">
<h3>{tier.toUpperCase()}</h3>
<p className="price">${price}/{cycle}</p>
<ul>
{features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
<button onClick={handleUpgrade}>
Upgrade to {tier}
</button>
</div>
);
};
Server-side: Create checkout session:
# Server-side: Checkout endpoint
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status, serializers
from django.conf import settings
import stripe
import logging
from datetime import datetime, timedelta
from apps.licenses.models import Tenant, License
logger = logging.getLogger(__name__)
stripe.api_key = settings.STRIPE_SECRET_KEY
# Request/Response Serializers
class CheckoutRequestSerializer(serializers.Serializer):
tier = serializers.ChoiceField(choices=['pro', 'team', 'enterprise'])
cycle = serializers.ChoiceField(choices=['monthly', 'annual'])
class CheckoutResponseSerializer(serializers.Serializer):
checkout_url = serializers.URLField()
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def create_checkout_session(request):
"""
Create Stripe Checkout session for subscription purchase.
Process:
1. Validate user and get tenant
2. Check for existing subscription
3. Create Stripe Checkout session
4. Return checkout URL
Returns:
Checkout URL for Stripe-hosted payment page
"""
# Validate request data
serializer = CheckoutRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"detail": "Invalid request data", "errors": serializer.errors},
status=status.HTTP_400_BAD_REQUEST
)
tier = serializer.validated_data['tier']
cycle = serializer.validated_data['cycle']
user = request.user
# Step 1: Get tenant
try:
tenant = Tenant.objects.get(owner_id=user.id)
except Tenant.DoesNotExist:
return Response(
{"detail": "Tenant not found"},
status=status.HTTP_404_NOT_FOUND
)
# Step 2: Check for existing subscription
existing_license = License.objects.filter(
tenant=tenant,
stripe_subscription_id__isnull=False,
is_active=True
).first()
if existing_license:
return Response(
{"detail": f'Already subscribed to {existing_license.tier} tier'},
status=status.HTTP_400_BAD_REQUEST
)
# Step 3: Get Stripe price ID
try:
price_id = get_stripe_price_id(tier, cycle)
except ValueError as e:
return Response(
{"detail": str(e)},
status=status.HTTP_400_BAD_REQUEST
)
# Step 4: Create Stripe Checkout session
try:
checkout_session = stripe.checkout.Session.create(
customer_email=tenant.billing_email,
mode='subscription',
line_items=[
{
'price': price_id,
'quantity': 1
}
],
subscription_data={
'metadata': {
'tenant_id': str(tenant.id),
'tier': tier
}
},
success_url=f'{settings.FRONTEND_URL}/billing/success?session_id={{CHECKOUT_SESSION_ID}}',
cancel_url=f'{settings.FRONTEND_URL}/pricing',
allow_promotion_codes=True, # Allow discount codes
billing_address_collection='required',
automatic_tax={'enabled': True}
)
return Response(
{'checkout_url': checkout_session.url},
status=status.HTTP_200_OK
)
except stripe.error.StripeError as e:
logger.exception(f"Stripe error: {e}")
return Response(
{"detail": "Payment system error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def get_stripe_price_id(tier: str, cycle: str) -> str:
"""
Get Stripe price ID for tier and billing cycle.
Price IDs are created in Stripe Dashboard.
"""
price_map = {
('pro', 'monthly'): 'price_1ProMonthly',
('pro', 'annual'): 'price_1ProAnnual',
('team', 'monthly'): 'price_1TeamMonthly',
('team', 'annual'): 'price_1TeamAnnual',
('enterprise', 'monthly'): 'price_1EnterpriseMonthly',
('enterprise', 'annual'): 'price_1EnterpriseAnnual',
}
price_id = price_map.get((tier, cycle))
if not price_id:
raise ValueError(f'Invalid tier/cycle: {tier}/{cycle}')
return price_id
2. Stripe Webhook Handler (Steps 10-15)
Server-side: Webhook endpoint:
# Server-side: Stripe webhook handler
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
import stripe
import logging
from datetime import timedelta
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([AllowAny]) # Webhook doesn't use auth, uses signature verification
@csrf_exempt
def stripe_webhook(request):
"""
Handle Stripe webhook events.
Events handled:
- checkout.session.completed: New subscription created
- invoice.paid: Subscription renewed
- invoice.payment_failed: Payment failed
- customer.subscription.deleted: Subscription cancelled
Security:
- Verify webhook signature (HMAC-SHA256)
- Idempotent processing (event IDs tracked)
"""
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
try:
# Verify webhook signature
event = stripe.Webhook.construct_event(
payload,
sig_header,
settings.STRIPE_WEBHOOK_SECRET
)
except ValueError:
# Invalid payload
return Response(
{"detail": "Invalid payload"},
status=status.HTTP_400_BAD_REQUEST
)
except stripe.error.SignatureVerificationError:
# Invalid signature
return Response(
{"detail": "Invalid signature"},
status=status.HTTP_400_BAD_REQUEST
)
# Handle event
event_type = event['type']
event_data = event['data']['object']
logger.info(f"Stripe webhook: {event_type}")
if event_type == 'checkout.session.completed':
handle_checkout_completed(event_data)
elif event_type == 'invoice.paid':
handle_invoice_paid(event_data)
elif event_type == 'invoice.payment_failed':
handle_payment_failed(event_data)
elif event_type == 'customer.subscription.deleted':
handle_subscription_deleted(event_data)
else:
logger.info(f"Unhandled event type: {event_type}")
return Response(
{'status': 'success'},
status=status.HTTP_200_OK
)
def handle_checkout_completed(session):
"""
Handle successful checkout completion.
Creates new license and activates subscription.
"""
from .models import Tenant, License, LicenseEvent
import secrets
subscription_id = session.get('subscription')
customer_id = session.get('customer')
metadata = session.get('subscription_data', {}).get('metadata', {}) or {}
tenant_id = metadata.get('tenant_id')
tier = metadata.get('tier', 'pro')
if not tenant_id:
logger.error(f"Missing tenant_id in checkout session: {session['id']}")
return
# Get tenant
tenant = await Tenant.objects.filter(id=tenant_id).afirst()
if not tenant:
logger.error(f"Tenant not found: {tenant_id}")
return
# Get subscription details from Stripe
subscription = stripe.Subscription.retrieve(subscription_id)
plan = subscription['items']['data'][0]['plan']
interval = plan['interval'] # 'month' or 'year'
# Calculate expiry date
if interval == 'month':
cycle = 'monthly'
expires_at = datetime.utcnow() + timedelta(days=30)
else: # year
cycle = 'annual'
expires_at = datetime.utcnow() + timedelta(days=365)
# Determine max seats based on tier
max_seats_map = {
'pro': 10,
'team': 50,
'enterprise': 500
}
max_seats = max_seats_map.get(tier, 10)
# Generate license key
license_key = f"lic-{secrets.token_urlsafe(32)}"
# Create license
license_obj = await License.objects.acreate(
tenant=tenant,
license_key=license_key,
tier=tier,
max_seats=max_seats,
subscription_cycle=cycle,
stripe_customer_id=customer_id,
stripe_subscription_id=subscription_id,
starts_at=datetime.utcnow(),
expires_at=expires_at,
is_active=True
)
# Record license event
await LicenseEvent.objects.acreate(
license=license_obj,
event_type='tier_change',
old_tier='free',
new_tier=tier,
metadata={
'subscription_id': subscription_id,
'customer_id': customer_id
}
)
# Send welcome email
from .email import send_welcome_email
await send_welcome_email(
email=tenant.billing_email,
license_key=license_key,
tier=tier,
expires_at=expires_at
)
# Update metrics
from prometheus_client import Counter, Gauge
new_subscriptions = Counter(
'new_subscriptions_total',
'Total new subscriptions',
['tier', 'cycle']
)
new_subscriptions.labels(tier=tier, cycle=cycle).inc()
mrr = Gauge(
'monthly_recurring_revenue',
'Monthly recurring revenue',
['tier']
)
# Update MRR (simplified - use Stripe reporting in production)
tier_prices = {
'pro': 49,
'team': 199,
'enterprise': 999
}
tier_price = tier_prices.get(tier, 0)
if cycle == 'annual':
# Convert annual to monthly
tier_price = tier_price / 12
mrr.labels(tier=tier).inc(tier_price)
logger.info(
f"License created: {license_key} "
f"(tenant: {tenant_id}, tier: {tier}, cycle: {cycle})"
)
Error Scenarios
Card Declined
Duplicate Subscription Attempt
Related Documentation
- ADR-013: Stripe Integration for Billing
- ADR-012: License Expiration and Renewal
- 07-trial-license-activation-flow.md: Trial conversion
- 08-license-renewal-flow.md: Subscription renewal
- 09-subscription-cancellation-flow.md: Cancellation handling
Last Updated: 2025-11-30 Diagram Type: Sequence (Mermaid) Scope: Billing - Stripe Checkout