/** * Billing Plans Hooks * * Provides access to the billing system's plans, features, and add-ons. * Used by platform admin for managing tenant subscriptions. */ import { useQuery } from '@tanstack/react-query'; import apiClient from '../api/client'; // Feature from billing system - the SINGLE SOURCE OF TRUTH export interface BillingFeature { id: number; code: string; name: string; description: string; feature_type: 'boolean' | 'integer'; // Dynamic feature management category: 'limits' | 'payments' | 'communication' | 'customization' | 'plugins' | 'advanced' | 'scheduling' | 'enterprise'; tenant_field_name: string; // Corresponding field on Tenant model display_order: number; is_overridable: boolean; depends_on: number | null; // ID of parent feature depends_on_code: string | null; // Code of parent feature (for convenience) } // Category metadata for display export const FEATURE_CATEGORY_META: Record = { limits: { label: 'Limits', order: 0 }, payments: { label: 'Payments & Revenue', order: 1 }, communication: { label: 'Communication', order: 2 }, customization: { label: 'Customization', order: 3 }, plugins: { label: 'Plugins & Automation', order: 4 }, advanced: { label: 'Advanced Features', order: 5 }, scheduling: { label: 'Scheduling', order: 6 }, enterprise: { label: 'Enterprise & Security', order: 7 }, }; // Plan feature with value export interface BillingPlanFeature { id: number; feature: BillingFeature; bool_value: boolean | null; int_value: number | null; value: boolean | number | null; } // Plan (logical grouping) export interface BillingPlan { id: number; code: string; name: string; description: string; display_order: number; is_active: boolean; max_pages: number; allow_custom_domains: boolean; max_custom_domains: number; } // Plan version (specific offer with pricing and features) export interface BillingPlanVersion { id: number; plan: BillingPlan; version: number; name: string; is_public: boolean; is_legacy: boolean; starts_at: string | null; ends_at: string | null; price_monthly_cents: number; price_yearly_cents: number; transaction_fee_percent: string; transaction_fee_fixed_cents: number; trial_days: number; sms_price_per_message_cents: number; masked_calling_price_per_minute_cents: number; proxy_number_monthly_fee_cents: number; default_auto_reload_enabled: boolean; default_auto_reload_threshold_cents: number; default_auto_reload_amount_cents: number; is_most_popular: boolean; show_price: boolean; marketing_features: string[]; stripe_product_id: string; stripe_price_id_monthly: string; stripe_price_id_yearly: string; is_available: boolean; features: BillingPlanFeature[]; subscriber_count?: number; created_at: string; } // Plan with all versions export interface BillingPlanWithVersions { id: number; code: string; name: string; description: string; display_order: number; is_active: boolean; max_pages: number; allow_custom_domains: boolean; max_custom_domains: number; versions: BillingPlanVersion[]; active_version: BillingPlanVersion | null; total_subscribers: number; } // Add-on product export interface BillingAddOn { id: number; code: string; name: string; description: string; price_monthly_cents: number; price_one_time_cents: number; stripe_product_id: string; stripe_price_id: string; is_stackable: boolean; is_active: boolean; features: BillingPlanFeature[]; } /** * Hook to get all billing plans with their versions (admin view) */ export const useBillingPlans = () => { return useQuery({ queryKey: ['billingPlans'], queryFn: async () => { const { data } = await apiClient.get('/billing/admin/plans/'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes }); }; /** * Hook to get the public plan catalog (available versions only) */ export const useBillingPlanCatalog = () => { return useQuery({ queryKey: ['billingPlanCatalog'], queryFn: async () => { const { data } = await apiClient.get('/billing/plans/'); return data; }, staleTime: 5 * 60 * 1000, }); }; /** * Hook to get all features */ export const useBillingFeatures = () => { return useQuery({ queryKey: ['billingFeatures'], queryFn: async () => { const { data } = await apiClient.get('/billing/admin/features/'); return data; }, staleTime: 10 * 60 * 1000, // 10 minutes (features rarely change) }); }; /** * Hook to get available add-ons */ export const useBillingAddOns = () => { return useQuery({ queryKey: ['billingAddOns'], queryFn: async () => { const { data } = await apiClient.get('/billing/addons/'); return data; }, staleTime: 5 * 60 * 1000, }); }; // ============================================================================= // Helper Functions // ============================================================================= /** * Get a feature value from a plan version's features array */ export function getFeatureValue( features: BillingPlanFeature[], featureCode: string ): boolean | number | null { const feature = features.find(f => f.feature.code === featureCode); if (!feature) return null; return feature.value; } /** * Get a boolean feature value (defaults to false if not found) */ export function getBooleanFeature( features: BillingPlanFeature[], featureCode: string ): boolean { const value = getFeatureValue(features, featureCode); return typeof value === 'boolean' ? value : false; } /** * Get an integer feature value (defaults to 0 if not found, null means unlimited) */ export function getIntegerFeature( features: BillingPlanFeature[], featureCode: string ): number | null { const value = getFeatureValue(features, featureCode); if (value === null || value === undefined) return null; // Unlimited return typeof value === 'number' ? value : 0; } /** * Convert a plan version's features to a flat object for form state * Maps feature codes to their values */ export function planFeaturesToFormState( planVersion: BillingPlanVersion | null ): Record { if (!planVersion) return {}; const state: Record = {}; for (const pf of planVersion.features) { state[pf.feature.code] = pf.value; } return state; } /** * Map old tier names to new plan codes */ export const TIER_TO_PLAN_CODE: Record = { FREE: 'free', STARTER: 'starter', GROWTH: 'growth', PROFESSIONAL: 'pro', // Old name -> new code PRO: 'pro', ENTERPRISE: 'enterprise', }; /** * Map new plan codes to display names */ export const PLAN_CODE_TO_NAME: Record = { free: 'Free', starter: 'Starter', growth: 'Growth', pro: 'Pro', enterprise: 'Enterprise', }; /** * Get the active plan version for a given plan code */ export function getActivePlanVersion( plans: BillingPlanWithVersions[], planCode: string ): BillingPlanVersion | null { const plan = plans.find(p => p.code === planCode); return plan?.active_version || null; } /** * Feature code mapping from old permission names to new feature codes */ export const PERMISSION_TO_FEATURE_CODE: Record = { // Communication can_use_sms_reminders: 'sms_enabled', can_use_masked_phone_numbers: 'masked_calling_enabled', // Platform can_api_access: 'api_access', can_use_custom_domain: 'custom_domain', can_white_label: 'remove_branding', // Features can_accept_payments: 'payment_processing', can_use_mobile_app: 'mobile_app_access', advanced_reporting: 'advanced_reporting', priority_support: 'priority_support', dedicated_support: 'dedicated_account_manager', // Limits (integer features) max_users: 'max_users', max_resources: 'max_resources', max_locations: 'max_locations', }; /** * Convert plan features to legacy permission format for backward compatibility */ export function planFeaturesToLegacyPermissions( planVersion: BillingPlanVersion | null ): Record { if (!planVersion) return {}; const permissions: Record = {}; // Map features to legacy permission names for (const pf of planVersion.features) { const code = pf.feature.code; const value = pf.value; // Direct feature code permissions[code] = value as boolean | number; // Also add with legacy naming for backward compatibility switch (code) { case 'sms_enabled': permissions.can_use_sms_reminders = value as boolean; break; case 'masked_calling_enabled': permissions.can_use_masked_phone_numbers = value as boolean; break; case 'api_access': permissions.can_api_access = value as boolean; permissions.can_connect_to_api = value as boolean; break; case 'custom_domain': permissions.can_use_custom_domain = value as boolean; break; case 'remove_branding': permissions.can_white_label = value as boolean; break; case 'payment_processing': permissions.can_accept_payments = value as boolean; break; case 'mobile_app_access': permissions.can_use_mobile_app = value as boolean; break; case 'advanced_reporting': permissions.advanced_reporting = value as boolean; break; case 'priority_support': permissions.priority_support = value as boolean; break; case 'dedicated_account_manager': permissions.dedicated_support = value as boolean; break; case 'integrations_enabled': permissions.can_use_webhooks = value as boolean; permissions.can_use_calendar_sync = value as boolean; break; case 'team_permissions': permissions.can_require_2fa = value as boolean; break; case 'audit_logs': permissions.can_download_logs = value as boolean; break; case 'custom_branding': permissions.can_customize_booking_page = value as boolean; break; case 'recurring_appointments': permissions.can_book_repeated_events = value as boolean; break; } } return permissions; }