# 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 ```bash # 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). ```python 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. ```python 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. ```python 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. ```python 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. ```python 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. ```python 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. ```python 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). ```python 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:** ```python 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 ```python 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 ```python 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:** ```python 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 ```python # 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 ```python 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