AI Skill Report Card

Designing Saas Billing Systems

A-85·Jan 24, 2026

SaaS Billing System Design

Python
# Core subscription model structure class Subscription: id: str customer_id: str plan_id: str status: str # active, canceled, past_due, trialing current_period_start: datetime current_period_end: datetime trial_end: datetime | None cancel_at_period_end: bool gateway_subscription_id: str created_at: datetime updated_at: datetime class Payment: id: str subscription_id: str amount: int # cents currency: str status: str # succeeded, failed, pending, refunded gateway_payment_id: str failure_reason: str | None processed_at: datetime
Recommendation
Add more concrete input/output examples for common scenarios like handling dunning management or calculating complex proration cases

1. System Design Phase

Progress:

  • Define subscription plans and pricing tiers
  • Design database schema (customers, plans, subscriptions, payments)
  • Choose payment gateway and configure webhooks
  • Define business rules (trials, grace periods, proration)
  • Plan admin dashboard requirements

2. Implementation Phase

Progress:

  • Implement core billing models
  • Build payment gateway integration
  • Create webhook handlers
  • Add subscription lifecycle management
  • Build financial reporting queries
  • Create admin interfaces

3. Testing & Monitoring

Progress:

  • Test payment flows (success, failure, retry)
  • Validate webhook reliability
  • Monitor key metrics (MRR, churn, failed payments)
  • Set up alerting for billing issues
Recommendation
Include specific error handling patterns for webhook failures and payment gateway timeouts with example code

Example 1: Subscription Upgrade with Proration Input: "Customer wants to upgrade from $29/month to $99/month mid-cycle" Output:

Python
def upgrade_subscription(subscription_id, new_plan_id): subscription = get_subscription(subscription_id) old_plan = subscription.plan new_plan = get_plan(new_plan_id) # Calculate proration days_remaining = (subscription.current_period_end - datetime.now()).days days_in_period = (subscription.current_period_end - subscription.current_period_start).days unused_amount = (old_plan.price * days_remaining) // days_in_period prorated_charge = new_plan.price - unused_amount # Update gateway subscription gateway.modify_subscription( subscription.gateway_subscription_id, new_plan_id, proration_amount=prorated_charge ) # Update local record subscription.plan_id = new_plan_id subscription.save()

Example 2: Failed Payment Retry Logic Input: "Handle failed payment with 3-retry strategy" Output:

Python
def handle_failed_payment(payment_id): payment = get_payment(payment_id) subscription = payment.subscription retry_count = payment.retry_count or 0 if retry_count < 3: # Schedule retry (1 day, 3 days, 7 days) retry_delay = [1, 3, 7][retry_count] schedule_payment_retry(payment_id, days=retry_delay) # Update subscription to past_due if first failure if retry_count == 0: subscription.status = 'past_due' subscription.save() # Notify customer send_failed_payment_email(subscription.customer) else: # Cancel subscription after 3 failed attempts subscription.status = 'canceled' subscription.canceled_at = datetime.now() subscription.save() send_cancellation_email(subscription.customer)
Recommendation
Provide template configuration examples for popular payment gateways beyond Stripe (PayPal, Paddle) to increase applicability

Core Tables

SQL
-- Subscription Plans CREATE TABLE plans ( id UUID PRIMARY KEY, name VARCHAR(100) NOT NULL, price INTEGER NOT NULL, -- cents currency VARCHAR(3) DEFAULT 'USD', interval VARCHAR(10) NOT NULL, -- 'month', 'year' trial_period_days INTEGER DEFAULT 0, active BOOLEAN DEFAULT true ); -- Customer Subscriptions CREATE TABLE subscriptions ( id UUID PRIMARY KEY, customer_id UUID NOT NULL, plan_id UUID REFERENCES plans(id), status VARCHAR(20) NOT NULL, current_period_start TIMESTAMP NOT NULL, current_period_end TIMESTAMP NOT NULL, trial_end TIMESTAMP, cancel_at_period_end BOOLEAN DEFAULT false, canceled_at TIMESTAMP, gateway_subscription_id VARCHAR(255), created_at TIMESTAMP DEFAULT NOW() ); -- Payment Records CREATE TABLE payments ( id UUID PRIMARY KEY, subscription_id UUID REFERENCES subscriptions(id), amount INTEGER NOT NULL, currency VARCHAR(3) NOT NULL, status VARCHAR(20) NOT NULL, gateway_payment_id VARCHAR(255), failure_code VARCHAR(50), failure_message TEXT, retry_count INTEGER DEFAULT 0, processed_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW() ); -- Usage Tracking (for usage-based billing) CREATE TABLE usage_records ( id UUID PRIMARY KEY, subscription_id UUID REFERENCES subscriptions(id), metric_name VARCHAR(100) NOT NULL, quantity INTEGER NOT NULL, recorded_at TIMESTAMP NOT NULL, period_start TIMESTAMP NOT NULL, period_end TIMESTAMP NOT NULL );

Stripe Integration Pattern

Python
class StripeGateway: def create_customer(self, email, name): return stripe.Customer.create(email=email, name=name) def create_subscription(self, customer_id, price_id, trial_days=None): params = { 'customer': customer_id, 'items': [{'price': price_id}], } if trial_days: params['trial_period_days'] = trial_days return stripe.Subscription.create(**params) def handle_webhook(self, event_type, event_data): handlers = { 'invoice.payment_succeeded': self._handle_payment_success, 'invoice.payment_failed': self._handle_payment_failure, 'customer.subscription.updated': self._handle_subscription_update, 'customer.subscription.deleted': self._handle_subscription_cancel, } handler = handlers.get(event_type) if handler: handler(event_data)
Python
def calculate_mrr(): """Monthly Recurring Revenue""" active_subscriptions = Subscription.objects.filter( status='active', cancel_at_period_end=False ) mrr = 0 for sub in active_subscriptions: if sub.plan.interval == 'month': mrr += sub.plan.price elif sub.plan.interval == 'year': mrr += sub.plan.price // 12 return mrr / 100 # Convert from cents def calculate_churn_rate(period_start, period_end): """Customer churn rate for given period""" customers_start = Subscription.objects.filter( created_at__lt=period_start, status='active' ).values('customer_id').distinct().count() customers_churned = Subscription.objects.filter( canceled_at__range=(period_start, period_end) ).values('customer_id').distinct().count() return (customers_churned / customers_start) * 100 if customers_start > 0 else 0 def calculate_ltv(average_monthly_revenue, churn_rate): """Customer Lifetime Value""" return average_monthly_revenue / (churn_rate / 100)
  • Idempotency: All payment operations must be idempotent using unique keys
  • Webhook Security: Always verify webhook signatures from payment gateways
  • Failed Payment Handling: Implement dunning management with email notifications
  • Proration Logic: Handle mid-cycle changes with proper proration calculations
  • Currency Precision: Store amounts in smallest currency unit (cents) as integers
  • Audit Trail: Log all subscription changes and payment events
  • Grace Periods: Allow reasonable grace periods before service suspension
  • Data Consistency: Use database transactions for multi-table billing operations
  • Race Conditions: Multiple webhook events can arrive simultaneously
  • Double Charging: Not handling webhook retries properly
  • Timezone Issues: Always use UTC for billing calculations
  • Plan Changes: Not handling proration correctly during upgrades/downgrades
  • Failed Payment Logic: Canceling immediately instead of implementing retry logic
  • Webhook Dependencies: Making webhook handlers dependent on external services
  • Currency Rounding: Incorrect decimal handling leading to penny discrepancies
  • Trial Abuse: Not preventing multiple trial signups from same user
Python
@csrf_exempt def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') try: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET ) except ValueError: return HttpResponse(status=400) except stripe.error.SignatureVerificationError: return HttpResponse(status=400) # Handle the event if event['type'] == 'invoice.payment_succeeded': invoice = event['data']['object'] handle_successful_payment(invoice) elif event['type'] == 'invoice.payment_failed': invoice = event['data']['object'] handle_failed_payment(invoice) return HttpResponse(status=200)
Python
def get_billing_dashboard_data(): return { 'mrr': calculate_mrr(), 'arr': calculate_mrr() * 12, 'active_subscriptions': Subscription.objects.filter(status='active').count(), 'churned_this_month': get_monthly_churn(), 'failed_payments_last_7_days': get_recent_failed_payments(), 'trial_conversions': get_trial_conversion_rate(), 'revenue_by_plan': get_revenue_breakdown_by_plan(), } def get_customer_billing_summary(customer_id): subscriptions = Subscription.objects.filter(customer_id=customer_id) payments = Payment.objects.filter(subscription__customer_id=customer_id) return { 'total_revenue': sum(p.amount for p in payments if p.status == 'succeeded'), 'subscription_history': list(subscriptions.values()), 'payment_history': list(payments.order_by('-created_at')[:10]), 'next_billing_date': subscriptions.filter(status='active').first().current_period_end, }

This skill provides comprehensive guidance for building production-ready SaaS billing systems with proper error handling, financial calculations, and administrative tools.

0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
11/15
Workflow
11/15
Examples
15/20
Completeness
15/20
Format
11/15
Conciseness
11/15