- 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>
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:
- Admin updates a plan version that has active subscribers
- System automatically:
- Marks old version as
is_legacy=True, is_public=False - Creates new version with updated pricing/features
- Marks old version as
- Existing subscribers keep their current version
- 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:
- Start with plan version features
- Layer add-on features (add-ons can extend but not reduce)
- 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:
- Creates invoice with plan snapshots
- Adds line item for base plan
- Adds line items for active add-ons
- Calculates totals
- 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 planPlanVersion.stripe_price_id_monthly- Monthly recurring PricePlanVersion.stripe_price_id_yearly- Annual recurring PriceAddOnProduct.stripe_product_id- Stripe Product for add-onAddOnProduct.stripe_price_id- Stripe Price for add-onSubscription.stripe_subscription_id- Active Stripe SubscriptionInvoice.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