import { apiClient } from '../client'; import { // New types SubscriptionTier, SUBSCRIPTION_TIERS, BillingCycle, PlanMetadata, AvailablePlans, UsageSummary, FeatureCheckResponse, QuotaCheckResponse, PlanUpgradeValidation, PlanUpgradeResult, doesPlanMeetMinimum, getPlanColor, getYearlyDiscountPercentage, PLAN_HIERARCHY, // Analytics levels ANALYTICS_LEVELS, AnalyticsLevel, ANALYTICS_HIERARCHY } from '../types/subscription'; // Map plan tiers to analytics levels based on backend data const TIER_TO_ANALYTICS_LEVEL: Record = { [SUBSCRIPTION_TIERS.STARTER]: ANALYTICS_LEVELS.BASIC, [SUBSCRIPTION_TIERS.PROFESSIONAL]: ANALYTICS_LEVELS.ADVANCED, [SUBSCRIPTION_TIERS.ENTERPRISE]: ANALYTICS_LEVELS.PREDICTIVE }; // Cache for available plans let cachedPlans: AvailablePlans | null = null; let lastFetchTime: number | null = null; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes export class SubscriptionService { private readonly baseUrl = '/tenants'; private readonly plansUrl = '/plans'; // ============================================================================ // NEW METHODS - Centralized Plans API // ============================================================================ /** * Invalidate cached plan data * Call this when subscription changes to ensure fresh data on next fetch */ invalidateCache(): void { cachedPlans = null; lastFetchTime = null; } /** * Fetch available subscription plans with complete metadata * Uses cached data if available and fresh (5 min cache) */ async fetchAvailablePlans(): Promise { const now = Date.now(); // Return cached data if it's still valid if (cachedPlans && lastFetchTime && (now - lastFetchTime) < CACHE_DURATION) { return cachedPlans; } try { const plans = await apiClient.get(this.plansUrl); cachedPlans = plans; lastFetchTime = now; return plans; } catch (error) { console.error('Failed to fetch subscription plans:', error); throw error; } } /** * Get metadata for a specific plan tier */ async getPlanMetadata(tier: SubscriptionTier): Promise { try { const plans = await this.fetchAvailablePlans(); return plans.plans[tier] || null; } catch (error) { console.error('Failed to get plan metadata:', error); return null; } } /** * Get all available features for a tier */ async getPlanFeatures(tier: SubscriptionTier): Promise { try { const metadata = await this.getPlanMetadata(tier); return metadata?.features || []; } catch (error) { console.error('Failed to get plan features:', error); return []; } } /** * Check if a feature is available in a tier */ async hasFeatureInTier(tier: SubscriptionTier, featureName: string): Promise { try { const features = await this.getPlanFeatures(tier); return features.includes(featureName); } catch (error) { console.error('Failed to check feature availability:', error); return false; } } /** * Get plan comparison data for pricing page */ async getPlanComparison(): Promise<{ tiers: SubscriptionTier[]; metadata: Record; }> { try { const plans = await this.fetchAvailablePlans(); return { tiers: [ SUBSCRIPTION_TIERS.STARTER, SUBSCRIPTION_TIERS.PROFESSIONAL, SUBSCRIPTION_TIERS.ENTERPRISE ], metadata: plans.plans }; } catch (error) { console.error('Failed to get plan comparison:', error); throw error; } } /** * Calculate savings for yearly billing */ calculateYearlySavings(monthlyPrice: number, yearlyPrice: number): { savingsAmount: number; savingsPercentage: number; monthsFree: number; } { const yearlyAnnual = monthlyPrice * 12; const savingsAmount = yearlyAnnual - yearlyPrice; const savingsPercentage = getYearlyDiscountPercentage(monthlyPrice, yearlyPrice); const monthsFree = Math.round(savingsAmount / monthlyPrice); return { savingsAmount, savingsPercentage, monthsFree }; } /** * Check if user's plan meets minimum requirement */ checkPlanMeetsMinimum(userPlan: SubscriptionTier, requiredPlan: SubscriptionTier): boolean { return doesPlanMeetMinimum(userPlan, requiredPlan); } /** * Get plan display color */ getPlanDisplayColor(tier: SubscriptionTier): string { return getPlanColor(tier); } // ============================================================================ // TENANT SUBSCRIPTION STATUS & USAGE // ============================================================================ /** * Get current usage summary for a tenant */ async getUsageSummary(tenantId: string): Promise { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/usage`); } /** * Check if tenant has access to a specific feature */ async checkFeatureAccess( tenantId: string, featureName: string ): Promise { return apiClient.get( `${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}/check` ); } /** * Check if tenant can perform an action within quota limits */ async checkQuotaLimit( tenantId: string, quotaType: string, requestedAmount?: number ): Promise { // Map quotaType to the existing endpoints in tenant_operations.py let endpoint: string; switch (quotaType) { case 'inventory_items': endpoint = 'can-add-product'; break; case 'users': endpoint = 'can-add-user'; break; case 'locations': endpoint = 'can-add-location'; break; default: throw new Error(`Unsupported quota type: ${quotaType}`); } const url = `${this.baseUrl}/subscriptions/${tenantId}/${endpoint}`; // Get the response from the endpoint (returns different format than expected) const response = await apiClient.get<{ can_add: boolean; current_count?: number; max_allowed?: number; reason?: string; message?: string; }>(url); // Map the response to QuotaCheckResponse format return { allowed: response.can_add, current: response.current_count || 0, limit: response.max_allowed || null, remaining: response.max_allowed !== undefined && response.current_count !== undefined ? response.max_allowed - response.current_count : null, message: response.reason || response.message || '' }; } async validatePlanUpgrade(tenantId: string, planKey: string): Promise { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/validate-upgrade/${planKey}`); } async upgradePlan(tenantId: string, planKey: string): Promise { return apiClient.post(`${this.baseUrl}/subscriptions/${tenantId}/upgrade?new_plan=${planKey}`, {}); } async canAddLocation(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-location`); } async canAddProduct(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-product`); } async canAddUser(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-user`); } async hasFeature(tenantId: string, featureName: string): Promise<{ has_feature: boolean; feature_value?: any; plan?: string; reason?: string }> { return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}`); } formatPrice(amount: number): string { return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(amount); } /** * Get plan display information */ async getPlanDisplayInfo(planKey: string) { try { const plans = await this.fetchAvailablePlans(); const plan = plans.plans[planKey as SubscriptionTier]; if (plan) { return { name: plan.name, color: this.getPlanColor(planKey as SubscriptionTier), description: plan.description, monthlyPrice: plan.monthly_price }; } return { name: 'Desconocido', color: 'gray', description: '', monthlyPrice: 0 }; } catch (error) { console.error('Failed to get plan display info:', error); return { name: 'Desconocido', color: 'gray', description: '', monthlyPrice: 0 }; } } /** * Get plan color based on plan key */ getPlanColor(planKey: string): string { switch (planKey) { case SUBSCRIPTION_TIERS.STARTER: return 'blue'; case SUBSCRIPTION_TIERS.PROFESSIONAL: return 'purple'; case SUBSCRIPTION_TIERS.ENTERPRISE: return 'amber'; default: return 'gray'; } } /** * Get analytics level for a plan tier */ getAnalyticsLevelForTier(tier: SubscriptionTier): AnalyticsLevel { return TIER_TO_ANALYTICS_LEVEL[tier] || ANALYTICS_LEVELS.NONE; } /** * Get analytics level for a plan (alias for getAnalyticsLevelForTier) * @deprecated Use getAnalyticsLevelForTier instead */ getAnalyticsLevelForPlan(tier: SubscriptionTier): AnalyticsLevel { return this.getAnalyticsLevelForTier(tier); } /** * Check if analytics level meets minimum requirements */ doesAnalyticsLevelMeetMinimum(level: AnalyticsLevel, minimumRequired: AnalyticsLevel): boolean { return ANALYTICS_HIERARCHY[level] >= ANALYTICS_HIERARCHY[minimumRequired]; } /** * Cancel subscription - Downgrade to read-only mode */ async cancelSubscription(tenantId: string, reason?: string): Promise<{ success: boolean; message: string; status: string; cancellation_effective_date: string; days_remaining: number; read_only_mode_starts: string; }> { return apiClient.post('/subscriptions/cancel', { tenant_id: tenantId, reason: reason || '' }); } /** * Reactivate a cancelled or inactive subscription */ async reactivateSubscription(tenantId: string, plan: string = 'starter'): Promise<{ success: boolean; message: string; status: string; plan: string; next_billing_date: string | null; }> { return apiClient.post('/subscriptions/reactivate', { tenant_id: tenantId, plan }); } /** * Get subscription status including read-only mode info */ async getSubscriptionStatus(tenantId: string): Promise<{ tenant_id: string; status: string; plan: string; is_read_only: boolean; cancellation_effective_date: string | null; days_until_inactive: number | null; }> { return apiClient.get(`/subscriptions/${tenantId}/status`); } } export const subscriptionService = new SubscriptionService();