Files
smoothschedule/smoothschedule/BILLING_PLANS.md
poduck fc63cf4fce Add platform email templates, staff invitations, and quota tracking
- Add PlatformEmailTemplate model and API for superuser-managed email templates
- Add PlatformStaffInvitation model with email sending via Celery tasks
- Add platform staff invite page and acceptance flow with auto-login
- Add quota tracking models (DailyAppointmentUsage, DailyAPIUsage, StorageUsage)
- Add quota status API endpoints and frontend banners
- Add storage usage service for tenant media tracking
- Fix platform user deletion with raw SQL to handle multi-tenant FK constraints
- Update EditPlatformUserModal with archive/delete buttons
- Update PlatformSidebar with email templates link for superusers
- Configure console email backend and Celery eager mode for local development

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 10:35:35 -05:00

18 KiB

SmoothSchedule Billing & Plans System

This document describes the architecture and capabilities of the billing and subscription management system.

Overview

The billing system supports:

  • Plans with Versioning - Grandfathering existing subscribers when prices change
  • Feature-Based Entitlements - Boolean (on/off) and integer (limits) features
  • Add-On Products - Purchasable extras that extend plan capabilities
  • Manual Overrides - Per-tenant entitlement grants for support/promos
  • Immutable Invoices - Snapshot-based billing records

Seeding the Catalog

# Seed/update the catalog (idempotent)
docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog

# Drop existing and reseed (for fresh start)
docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog --drop-existing

Current Plan Catalog

Plans Overview

Plan Monthly Annual Target User
Free $0 $0 Solo practitioners testing the platform
Starter $19 $190 Small businesses getting started
Growth $59 $590 Growing teams needing SMS & integrations
Pro $99 $990 Established businesses needing API & analytics
Enterprise $199 $1,990 Multi-location or white-label needs

Annual pricing = ~10x monthly (2 months free)

Feature Matrix - Boolean Features

Feature Free Starter Growth Pro Enterprise
email_enabled Yes Yes Yes Yes Yes
online_booking Yes Yes Yes Yes Yes
recurring_appointments Yes Yes Yes Yes Yes
payment_processing - Yes Yes Yes Yes
mobile_app_access - Yes Yes Yes Yes
sms_enabled - - Yes Yes Yes
custom_domain - - Yes Yes Yes
integrations_enabled - - Yes Yes Yes
api_access - - - Yes Yes
masked_calling_enabled - - - Yes Yes
advanced_reporting - - - Yes Yes
team_permissions - - - Yes Yes
audit_logs - - - Yes Yes
can_white_label - - - Yes Yes
multi_location - - - - Yes
priority_support - - - - Yes
dedicated_account_manager - - - - Yes
sla_guarantee - - - - Yes

Feature Matrix - Integer Limits

Limit Free Starter Growth Pro Enterprise
max_users 1 3 10 25 0 (unlimited)
max_resources 1 5 15 50 0 (unlimited)
max_locations 1 1 3 10 0 (unlimited)
max_services 3 10 25 100 0 (unlimited)
max_customers 50 500 2000 10000 0 (unlimited)
max_appointments_per_month 50 200 1000 5000 0 (unlimited)
max_sms_per_month 0 0 500 2000 10000
max_email_per_month 100 500 2000 10000 0 (unlimited)
max_storage_mb 100 500 2000 10000 0 (unlimited)
max_api_calls_per_day 0 0 1000 10000 0 (unlimited)

Note: 0 = unlimited for integer limits.

Add-Ons

Add-On Price Effect Stackable Eligible Plans
SMS Boost (+5,000 SMS) $25/mo, $250/yr +5,000 max_sms_per_month Yes Growth, Pro, Enterprise
Extra Locations (+5) $29/mo, $290/yr +5 max_locations Yes Growth, Pro, Enterprise
Advanced Reporting $15/mo, $150/yr Enables advanced_reporting No Starter, Growth
API Access $20/mo, $200/yr Enables api_access, 5K API calls/day No Starter, Growth
Masked Calling $39/mo, $390/yr Enables masked_calling_enabled No Starter, Growth
White Label $99/mo, $990/yr Enables can_white_label (custom branding + remove branding) No Pro only

Stackable add-ons: Integer values multiply by quantity purchased.


Architecture

                    ┌─────────────────┐
                    │     Tenant      │
                    │   (Business)    │
                    └────────┬────────┘
                             │
                             │ has one
                             ▼
                    ┌─────────────────┐
                    │  Subscription   │
                    │                 │
                    │ - status        │
                    │ - trial_ends_at │
                    │ - period dates  │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
     ┌─────────────┐  ┌───────────┐  ┌──────────────┐
     │ PlanVersion │  │ AddOns    │  │  Invoices    │
     │             │  │ (M2M)     │  │              │
     └──────┬──────┘  └───────────┘  └──────────────┘
            │
            │ belongs to
            ▼
     ┌─────────────┐
     │    Plan     │
     │             │
     │ - code      │
     │ - name      │
     └─────────────┘

Core Models

Plan

Logical grouping representing a tier (e.g., Free, Starter, Pro, Enterprise).

Plan:
    code: str           # Unique identifier (e.g., "pro")
    name: str           # Display name (e.g., "Pro")
    description: str    # Marketing description
    display_order: int  # Sort order in UI
    is_active: bool     # Available for new signups

PlanVersion

A specific version of a plan with pricing and features. Enables grandfathering.

PlanVersion:
    plan: FK(Plan)
    version: int                        # Auto-incremented per plan
    name: str                           # "Pro Plan v2"

    # Availability
    is_public: bool                     # Visible in catalog
    is_legacy: bool                     # Hidden from new signups (grandfathered)
    starts_at: datetime                 # Optional availability window
    ends_at: datetime

    # Pricing (in cents)
    price_monthly_cents: int            # Monthly subscription price
    price_yearly_cents: int             # Annual subscription price

    # Transaction Fees
    transaction_fee_percent: Decimal    # Platform fee percentage
    transaction_fee_fixed_cents: int    # Fixed fee per transaction

    # Trial
    trial_days: int                     # Free trial duration

    # Communication Pricing (usage-based)
    sms_price_per_message_cents: int
    masked_calling_price_per_minute_cents: int
    proxy_number_monthly_fee_cents: int

    # Credit Auto-Reload Defaults
    default_auto_reload_enabled: bool
    default_auto_reload_threshold_cents: int
    default_auto_reload_amount_cents: int

    # Display Settings
    is_most_popular: bool               # Highlight in pricing page
    show_price: bool                    # Show price or "Contact us"
    marketing_features: list            # Bullet points for pricing page

    # Stripe Integration
    stripe_product_id: str
    stripe_price_id_monthly: str
    stripe_price_id_yearly: str

Grandfathering Flow:

  1. Admin updates a plan version that has active subscribers
  2. System automatically:
    • Marks old version as is_legacy=True, is_public=False
    • Creates new version with updated pricing/features
  3. Existing subscribers keep their current version
  4. New subscribers get the new version

Feature

Single source of truth for all capabilities and limits.

Feature:
    code: str           # Unique identifier (e.g., "sms_enabled")
    name: str           # Display name
    description: str    # What this feature enables
    feature_type: str   # "boolean" or "integer"

Feature Types:

  • Boolean: On/off capabilities (e.g., sms_enabled, api_access)
  • Integer: Numeric limits (e.g., max_users, max_appointments_per_month)

PlanFeature (M2M)

Links features to plan versions with their values.

PlanFeature:
    plan_version: FK(PlanVersion)
    feature: FK(Feature)
    bool_value: bool    # For boolean features
    int_value: int      # For integer features

Subscription

Links a tenant to their plan version.

Subscription:
    business: FK(Tenant)
    plan_version: FK(PlanVersion)
    status: str                     # "active", "trial", "past_due", "canceled", "paused"
    started_at: datetime
    current_period_start: datetime
    current_period_end: datetime
    trial_ends_at: datetime
    canceled_at: datetime
    stripe_subscription_id: str

AddOnProduct

Purchasable extras that extend plan capabilities.

AddOnProduct:
    code: str                       # Unique identifier
    name: str                       # Display name
    description: str
    price_monthly_cents: int        # Recurring price
    price_one_time_cents: int       # One-time purchase price
    is_active: bool
    stripe_product_id: str
    stripe_price_id: str

SubscriptionAddOn (M2M)

Links add-ons to subscriptions.

SubscriptionAddOn:
    subscription: FK(Subscription)
    addon: FK(AddOnProduct)
    status: str                     # "active", "canceled", "expired"
    activated_at: datetime
    expires_at: datetime            # For time-limited add-ons

EntitlementOverride

Manual per-tenant feature grants (support tickets, promos, partnerships).

EntitlementOverride:
    business: FK(Tenant)
    feature: FK(Feature)
    bool_value: bool
    int_value: int
    reason: str                     # "support_ticket", "promo", "partnership", "manual"
    notes: str                      # Admin notes
    granted_by: FK(User)
    expires_at: datetime            # Optional expiration

Entitlement Resolution

The EntitlementService resolves effective entitlements with this precedence:

Override > Add-on > Plan

Resolution Logic:

  1. Start with plan version features
  2. Layer add-on features (add-ons can extend but not reduce)
  3. Apply manual overrides (highest priority)

API:

from smoothschedule.billing.services.entitlements import EntitlementService

# Get all effective entitlements for a tenant
entitlements = EntitlementService.get_effective_entitlements(tenant)
# Returns: {"sms_enabled": True, "max_users": 25, ...}

# Check a specific boolean feature
can_use_sms = EntitlementService.has_feature(tenant, "sms_enabled")

# Get a specific limit
max_users = EntitlementService.get_limit(tenant, "max_users")

Seeded Features

The system seeds 30 features (20 boolean, 10 integer):

Boolean Features (Capabilities)

Code Description
sms_enabled Can send SMS notifications
email_enabled Can send email notifications
masked_calling_enabled Can use masked phone calls
api_access Can access REST API
custom_domain Can use custom domain
can_white_label Customize branding and remove "Powered by"
multi_location Can manage multiple locations
advanced_reporting Access to analytics dashboard
priority_support Priority support queue
dedicated_account_manager Has dedicated AM
sla_guarantee SLA commitments
team_permissions Granular team permissions
audit_logs Access to audit logs
integrations_enabled Can use third-party integrations
mobile_app_access Field staff mobile app
online_booking Customer self-booking
payment_processing Accept payments
recurring_appointments Recurring bookings

Integer Features (Limits)

Code Description
max_users Maximum team members
max_resources Maximum resources/equipment
max_locations Maximum business locations
max_services Maximum service types
max_customers Maximum customer records
max_appointments_per_month Monthly appointment limit
max_sms_per_month Monthly SMS limit
max_email_per_month Monthly email limit
max_storage_mb File storage limit
max_api_calls_per_day Daily API rate limit

Invoice System

Invoices capture immutable snapshots of billing data.

Invoice

Invoice:
    business: FK(Tenant)
    subscription: FK(Subscription)
    period_start: datetime
    period_end: datetime

    # Snapshot values (immutable)
    plan_code_at_billing: str
    plan_name_at_billing: str
    plan_version_id_at_billing: int

    # Amounts (in cents)
    subtotal_amount: int
    discount_amount: int
    tax_amount: int
    total_amount: int

    status: str                     # "draft", "pending", "paid", "failed", "refunded"
    currency: str                   # "USD"
    stripe_invoice_id: str
    paid_at: datetime

InvoiceLine

InvoiceLine:
    invoice: FK(Invoice)
    line_type: str                  # "plan", "addon", "usage", "credit", "adjustment"
    description: str
    quantity: int
    unit_amount: int
    subtotal_amount: int
    tax_amount: int
    total_amount: int

    # References (for audit trail)
    plan_version: FK(PlanVersion)   # If line_type="plan"
    addon: FK(AddOnProduct)         # If line_type="addon"
    feature_code: str               # If line_type="usage"
    metadata: JSON                  # Additional context

Invoice Generation:

from smoothschedule.billing.services.invoicing import generate_invoice_for_subscription

invoice = generate_invoice_for_subscription(
    subscription,
    period_start,
    period_end
)

The service:

  1. Creates invoice with plan snapshots
  2. Adds line item for base plan
  3. Adds line items for active add-ons
  4. Calculates totals
  5. Returns immutable invoice

API Endpoints

Public Endpoints

Endpoint Method Description
/api/billing/plans/ GET List available plans (catalog)
/api/billing/addons/ GET List available add-ons

Authenticated Endpoints

Endpoint Method Description
/api/me/entitlements/ GET Current tenant's entitlements
/api/me/subscription/ GET Current subscription details
/api/billing/invoices/ GET List tenant's invoices
/api/billing/invoices/{id}/ GET Invoice detail with line items

Admin Endpoints (Platform Admin Only)

Endpoint Method Description
/api/billing/admin/features/ CRUD Manage features
/api/billing/admin/plans/ CRUD Manage plans
/api/billing/admin/plans/{id}/create_version/ POST Create new plan version
/api/billing/admin/plan-versions/ CRUD Manage plan versions
/api/billing/admin/plan-versions/{id}/mark_legacy/ POST Mark version as legacy
/api/billing/admin/plan-versions/{id}/subscribers/ GET List version subscribers
/api/billing/admin/addons/ CRUD Manage add-on products

Stripe Integration Points

The system stores Stripe IDs for synchronization:

  • PlanVersion.stripe_product_id - Stripe Product for the plan
  • PlanVersion.stripe_price_id_monthly - Monthly recurring Price
  • PlanVersion.stripe_price_id_yearly - Annual recurring Price
  • AddOnProduct.stripe_product_id - Stripe Product for add-on
  • AddOnProduct.stripe_price_id - Stripe Price for add-on
  • Subscription.stripe_subscription_id - Active Stripe Subscription
  • Invoice.stripe_invoice_id - Stripe Invoice reference

Usage Example

Creating a Plan with Features

# 1. Create the plan
plan = Plan.objects.create(
    code="pro",
    name="Pro",
    description="For growing businesses",
    display_order=2,
    is_active=True
)

# 2. Create a plan version with pricing
version = PlanVersion.objects.create(
    plan=plan,
    version=1,
    name="Pro Plan",
    is_public=True,
    price_monthly_cents=2999,  # $29.99/month
    price_yearly_cents=29990,  # $299.90/year
    trial_days=14,
    is_most_popular=True,
    marketing_features=[
        "Unlimited appointments",
        "SMS reminders",
        "Custom branding",
        "Priority support"
    ]
)

# 3. Attach features
PlanFeature.objects.create(
    plan_version=version,
    feature=Feature.objects.get(code="sms_enabled"),
    bool_value=True
)
PlanFeature.objects.create(
    plan_version=version,
    feature=Feature.objects.get(code="max_users"),
    int_value=10
)

Checking Entitlements in Code

from smoothschedule.billing.services.entitlements import EntitlementService

def send_sms_reminder(tenant, appointment):
    # Check if tenant can use SMS
    if not EntitlementService.has_feature(tenant, "sms_enabled"):
        raise PermissionDenied("SMS not available on your plan")

    # Check monthly limit
    current_usage = get_sms_usage_this_month(tenant)
    limit = EntitlementService.get_limit(tenant, "max_sms_per_month")

    if limit and current_usage >= limit:
        raise PermissionDenied("Monthly SMS limit reached")

    # Send the SMS...

Future Enhancements

Planned but not yet implemented:

  • Stripe webhook handlers for subscription lifecycle
  • Proration for mid-cycle upgrades/downgrades
  • Usage-based billing for SMS/calling
  • Credit system for prepaid usage
  • Coupon/discount code support
  • Tax calculation integration
  • Dunning management for failed payments