- Add is_stackable field to AddOnProduct model for add-ons that can be purchased multiple times - Add quantity field to SubscriptionAddOn for tracking purchase count - Update EntitlementService to ADD integer add-on values to base plan (instead of max) and multiply by quantity for stackable add-ons - Add feature selection to AddOnEditorModal using FeaturePicker component - Add AddOnFeatureSerializer for nested feature CRUD on add-ons - Fix Create Add-on button styling to use solid blue (was muted outline) - Widen billing sidebar from 320px to 384px to prevent text wrapping 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
446 lines
11 KiB
TypeScript
446 lines
11 KiB
TypeScript
/**
|
|
* Canonical Feature Catalog
|
|
*
|
|
* This file defines the canonical list of features available in the SmoothSchedule
|
|
* billing system. Features are organized by type (boolean vs integer) and include
|
|
* human-readable labels and descriptions.
|
|
*
|
|
* IMPORTANT: When adding new feature codes, add them here first to maintain a
|
|
* single source of truth. The FeaturePicker component uses this catalog to
|
|
* provide autocomplete and validation.
|
|
*
|
|
* Feature Types:
|
|
* - Boolean: On/off capabilities (e.g., sms_enabled, api_access)
|
|
* - Integer: Limit/quota features (e.g., max_users, max_resources)
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* import { FEATURE_CATALOG, getFeatureInfo, isCanonicalFeature } from '../billing/featureCatalog';
|
|
*
|
|
* // Get info about a feature
|
|
* const info = getFeatureInfo('max_users');
|
|
* // { code: 'max_users', name: 'Maximum Users', type: 'integer', ... }
|
|
*
|
|
* // Check if a feature is in the canonical catalog
|
|
* const isCanonical = isCanonicalFeature('custom_feature'); // false
|
|
* ```
|
|
*/
|
|
|
|
export type FeatureType = 'boolean' | 'integer';
|
|
|
|
export interface FeatureCatalogEntry {
|
|
code: string;
|
|
name: string;
|
|
description: string;
|
|
type: FeatureType;
|
|
category: FeatureCategory;
|
|
}
|
|
|
|
export type FeatureCategory =
|
|
| 'communication'
|
|
| 'limits'
|
|
| 'access'
|
|
| 'branding'
|
|
| 'support'
|
|
| 'integrations'
|
|
| 'security'
|
|
| 'scheduling';
|
|
|
|
// =============================================================================
|
|
// Boolean Features (Capabilities)
|
|
// =============================================================================
|
|
|
|
export const BOOLEAN_FEATURES: FeatureCatalogEntry[] = [
|
|
// Communication
|
|
{
|
|
code: 'sms_enabled',
|
|
name: 'SMS Messaging',
|
|
description: 'Send SMS notifications and reminders to customers',
|
|
type: 'boolean',
|
|
category: 'communication',
|
|
},
|
|
{
|
|
code: 'masked_calling_enabled',
|
|
name: 'Masked Calling',
|
|
description: 'Make calls with masked caller ID for privacy',
|
|
type: 'boolean',
|
|
category: 'communication',
|
|
},
|
|
{
|
|
code: 'proxy_number_enabled',
|
|
name: 'Proxy Phone Numbers',
|
|
description: 'Use proxy phone numbers for customer communication',
|
|
type: 'boolean',
|
|
category: 'communication',
|
|
},
|
|
|
|
// Payments & Commerce
|
|
{
|
|
code: 'can_accept_payments',
|
|
name: 'Accept Payments',
|
|
description: 'Accept online payments via Stripe Connect',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_use_pos',
|
|
name: 'Point of Sale',
|
|
description: 'Use Point of Sale (POS) system',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
|
|
// Scheduling & Booking
|
|
{
|
|
code: 'recurring_appointments',
|
|
name: 'Recurring Appointments',
|
|
description: 'Schedule recurring appointments',
|
|
type: 'boolean',
|
|
category: 'scheduling',
|
|
},
|
|
{
|
|
code: 'group_bookings',
|
|
name: 'Group Bookings',
|
|
description: 'Allow multiple customers per appointment',
|
|
type: 'boolean',
|
|
category: 'scheduling',
|
|
},
|
|
{
|
|
code: 'waitlist',
|
|
name: 'Waitlist',
|
|
description: 'Enable waitlist for fully booked slots',
|
|
type: 'boolean',
|
|
category: 'scheduling',
|
|
},
|
|
{
|
|
code: 'can_add_video_conferencing',
|
|
name: 'Video Conferencing',
|
|
description: 'Add video conferencing to events',
|
|
type: 'boolean',
|
|
category: 'scheduling',
|
|
},
|
|
|
|
// Access & Features
|
|
{
|
|
code: 'api_access',
|
|
name: 'API Access',
|
|
description: 'Access the public API for integrations',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_use_analytics',
|
|
name: 'Analytics Dashboard',
|
|
description: 'Access business analytics and reporting',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_use_tasks',
|
|
name: 'Automated Tasks',
|
|
description: 'Create and run automated task workflows',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_use_contracts',
|
|
name: 'Contracts & E-Signatures',
|
|
description: 'Create and manage e-signature contracts',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'customer_portal',
|
|
name: 'Customer Portal',
|
|
description: 'Branded self-service portal for customers',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'custom_fields',
|
|
name: 'Custom Fields',
|
|
description: 'Create custom data fields for resources and events',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_export_data',
|
|
name: 'Data Export',
|
|
description: 'Export data (appointments, customers, etc.)',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
{
|
|
code: 'can_use_mobile_app',
|
|
name: 'Mobile App',
|
|
description: 'Access the mobile app for field employees',
|
|
type: 'boolean',
|
|
category: 'access',
|
|
},
|
|
|
|
// Integrations
|
|
{
|
|
code: 'calendar_sync',
|
|
name: 'Calendar Sync',
|
|
description: 'Sync with Google Calendar, Outlook, etc.',
|
|
type: 'boolean',
|
|
category: 'integrations',
|
|
},
|
|
{
|
|
code: 'webhooks_enabled',
|
|
name: 'Webhooks',
|
|
description: 'Send webhook notifications for events',
|
|
type: 'boolean',
|
|
category: 'integrations',
|
|
},
|
|
{
|
|
code: 'can_use_plugins',
|
|
name: 'Plugin Integrations',
|
|
description: 'Use third-party plugin integrations',
|
|
type: 'boolean',
|
|
category: 'integrations',
|
|
},
|
|
{
|
|
code: 'can_create_plugins',
|
|
name: 'Create Plugins',
|
|
description: 'Create custom plugins for automation',
|
|
type: 'boolean',
|
|
category: 'integrations',
|
|
},
|
|
{
|
|
code: 'can_manage_oauth_credentials',
|
|
name: 'Manage OAuth',
|
|
description: 'Manage your own OAuth credentials',
|
|
type: 'boolean',
|
|
category: 'integrations',
|
|
},
|
|
|
|
// Branding
|
|
{
|
|
code: 'custom_branding',
|
|
name: 'Custom Branding',
|
|
description: 'Customize branding colors, logo, and styling',
|
|
type: 'boolean',
|
|
category: 'branding',
|
|
},
|
|
{
|
|
code: 'white_label',
|
|
name: 'White Label',
|
|
description: 'Remove SmoothSchedule branding completely',
|
|
type: 'boolean',
|
|
category: 'branding',
|
|
},
|
|
{
|
|
code: 'can_use_custom_domain',
|
|
name: 'Custom Domain',
|
|
description: 'Configure a custom domain for your booking page',
|
|
type: 'boolean',
|
|
category: 'branding',
|
|
},
|
|
|
|
// Support
|
|
{
|
|
code: 'priority_support',
|
|
name: 'Priority Support',
|
|
description: 'Get priority customer support response',
|
|
type: 'boolean',
|
|
category: 'support',
|
|
},
|
|
|
|
// Security & Compliance
|
|
{
|
|
code: 'can_require_2fa',
|
|
name: 'Require 2FA',
|
|
description: 'Require two-factor authentication for users',
|
|
type: 'boolean',
|
|
category: 'security',
|
|
},
|
|
{
|
|
code: 'sso_enabled',
|
|
name: 'Single Sign-On (SSO)',
|
|
description: 'Enable SSO authentication for team members',
|
|
type: 'boolean',
|
|
category: 'security',
|
|
},
|
|
{
|
|
code: 'can_delete_data',
|
|
name: 'Delete Data',
|
|
description: 'Permanently delete data',
|
|
type: 'boolean',
|
|
category: 'security',
|
|
},
|
|
{
|
|
code: 'can_download_logs',
|
|
name: 'Download Logs',
|
|
description: 'Download system logs',
|
|
type: 'boolean',
|
|
category: 'security',
|
|
},
|
|
];
|
|
|
|
// =============================================================================
|
|
// Integer Features (Limits & Quotas)
|
|
// =============================================================================
|
|
|
|
export const INTEGER_FEATURES: FeatureCatalogEntry[] = [
|
|
// User/Resource Limits
|
|
{
|
|
code: 'max_users',
|
|
name: 'Maximum Team Members',
|
|
description: 'Maximum number of team member accounts (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_resources',
|
|
name: 'Maximum Resources',
|
|
description: 'Maximum number of resources (staff, rooms, equipment)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_locations',
|
|
name: 'Location Limit',
|
|
description: 'Maximum number of business locations (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_services',
|
|
name: 'Maximum Services',
|
|
description: 'Maximum number of service types (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_customers',
|
|
name: 'Customer Limit',
|
|
description: 'Maximum number of customer records (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_event_types',
|
|
name: 'Max Event Types',
|
|
description: 'Maximum number of event types',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
|
|
// Usage Limits
|
|
{
|
|
code: 'max_appointments_per_month',
|
|
name: 'Monthly Appointment Limit',
|
|
description: 'Maximum appointments per month (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_automated_tasks',
|
|
name: 'Automated Task Limit',
|
|
description: 'Maximum number of automated tasks (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_email_templates',
|
|
name: 'Email Template Limit',
|
|
description: 'Maximum number of custom email templates (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_calendars_connected',
|
|
name: 'Max Calendars',
|
|
description: 'Maximum number of external calendars connected',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
|
|
// Technical Limits
|
|
{
|
|
code: 'storage_gb',
|
|
name: 'Storage (GB)',
|
|
description: 'File storage limit in gigabytes (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
{
|
|
code: 'max_api_requests_per_day',
|
|
name: 'Daily API Request Limit',
|
|
description: 'Maximum API requests per day (0 = unlimited)',
|
|
type: 'integer',
|
|
category: 'limits',
|
|
},
|
|
];
|
|
|
|
// =============================================================================
|
|
// Combined Catalog
|
|
// =============================================================================
|
|
|
|
export const FEATURE_CATALOG: FeatureCatalogEntry[] = [
|
|
...BOOLEAN_FEATURES,
|
|
...INTEGER_FEATURES,
|
|
];
|
|
|
|
// Create a lookup map for quick access
|
|
const featureMap = new Map<string, FeatureCatalogEntry>(
|
|
FEATURE_CATALOG.map((f) => [f.code, f])
|
|
);
|
|
|
|
// =============================================================================
|
|
// Helper Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Get feature information by code
|
|
*/
|
|
export const getFeatureInfo = (code: string): FeatureCatalogEntry | undefined => {
|
|
return featureMap.get(code);
|
|
};
|
|
|
|
/**
|
|
* Check if a feature code is in the canonical catalog
|
|
*/
|
|
export const isCanonicalFeature = (code: string): boolean => {
|
|
return featureMap.has(code);
|
|
};
|
|
|
|
/**
|
|
* Get all features by type
|
|
*/
|
|
export const getFeaturesByType = (type: FeatureType): FeatureCatalogEntry[] => {
|
|
return FEATURE_CATALOG.filter((f) => f.type === type);
|
|
};
|
|
|
|
/**
|
|
* Get all features by category
|
|
*/
|
|
export const getFeaturesByCategory = (category: FeatureCategory): FeatureCatalogEntry[] => {
|
|
return FEATURE_CATALOG.filter((f) => f.category === category);
|
|
};
|
|
|
|
/**
|
|
* Get all unique categories
|
|
*/
|
|
export const getAllCategories = (): FeatureCategory[] => {
|
|
return [...new Set(FEATURE_CATALOG.map((f) => f.category))];
|
|
};
|
|
|
|
/**
|
|
* Format category name for display
|
|
*/
|
|
export const formatCategoryName = (category: FeatureCategory): string => {
|
|
const names: Record<FeatureCategory, string> = {
|
|
communication: 'Communication',
|
|
limits: 'Limits & Quotas',
|
|
access: 'Access & Features',
|
|
branding: 'Branding & Customization',
|
|
support: 'Support',
|
|
integrations: 'Integrations',
|
|
security: 'Security & Compliance',
|
|
scheduling: 'Scheduling & Booking',
|
|
};
|
|
return names[category];
|
|
};
|