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, 'demo': ANALYTICS_LEVELS.ADVANCED, // Treat demo tier same as professional for analytics access }; // 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; billing_cycle?: string; next_billing_date?: string; }> { return apiClient.get(`/subscriptions/${tenantId}/status`); } /** * Get invoice history for a tenant */ async getInvoices(tenantId: string): Promise> { return apiClient.get(`/subscriptions/${tenantId}/invoices`); } /** * Update the default payment method for a subscription */ async updatePaymentMethod( tenantId: string, paymentMethodId: string ): Promise<{ success: boolean; message: string; payment_method_id: string; brand: string; last4: string; exp_month?: number; exp_year?: number; }> { return apiClient.post(`/subscriptions/${tenantId}/update-payment-method?payment_method_id=${paymentMethodId}`, {}); } // ============================================================================ // NEW METHODS - Usage Forecasting & Predictive Analytics // ============================================================================ /** * Get usage forecast for all metrics * Returns predictions for when tenant will hit limits based on growth rate */ async getUsageForecast(tenantId: string): Promise<{ tenant_id: string; forecasted_at: string; metrics: Array<{ metric: string; label: string; current: number; limit: number | null; unit: string; daily_growth_rate: number | null; predicted_breach_date: string | null; days_until_breach: number | null; usage_percentage: number; status: string; trend_data: Array<{ date: string; value: number }>; }>; }> { return apiClient.get(`/usage-forecast?tenant_id=${tenantId}`); } /** * Track daily usage (called by cron jobs or manually) * Stores usage snapshots in Redis for trend analysis */ async trackDailyUsage( tenantId: string, metric: string, value: number ): Promise<{ success: boolean; tenant_id: string; metric: string; value: number; date: string; }> { return apiClient.post('/usage-forecast/track-usage', { tenant_id: tenantId, metric, value, }); } /** * Get current subscription for a tenant * Combines subscription data with available plans metadata */ async getCurrentSubscription(tenantId: string): Promise<{ tier: SubscriptionTier; billing_cycle: 'monthly' | 'yearly'; monthly_price: number; yearly_price: number; renewal_date: string; trial_ends_at?: string; limits: { users: number | null; locations: number | null; products: number | null; recipes: number | null; suppliers: number | null; trainingJobsPerDay: number | null; forecastsPerDay: number | null; storageGB: number | null; }; availablePlans: AvailablePlans; }> { // Fetch both subscription status and available plans const [status, plans] = await Promise.all([ this.getSubscriptionStatus(tenantId), this.fetchAvailablePlans(), ]); const currentPlan = plans.plans[status.plan as SubscriptionTier]; return { tier: status.plan as SubscriptionTier, billing_cycle: (status.billing_cycle as 'monthly' | 'yearly') || 'monthly', monthly_price: currentPlan?.monthly_price || 0, yearly_price: currentPlan?.yearly_price || 0, renewal_date: status.next_billing_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), limits: { users: currentPlan?.limits?.users ?? null, locations: currentPlan?.limits?.locations ?? null, products: currentPlan?.limits?.products ?? null, recipes: currentPlan?.limits?.recipes ?? null, suppliers: currentPlan?.limits?.suppliers ?? null, trainingJobsPerDay: currentPlan?.limits?.training_jobs_per_day ?? null, forecastsPerDay: currentPlan?.limits?.forecasts_per_day ?? null, storageGB: currentPlan?.limits?.storage_gb ?? null, }, availablePlans: plans, }; } } export const subscriptionService = new SubscriptionService();