2025-09-05 17:49:48 +02:00
|
|
|
import { apiClient } from '../client';
|
2025-09-20 08:59:12 +02:00
|
|
|
import {
|
|
|
|
|
SubscriptionLimits,
|
|
|
|
|
FeatureCheckResponse,
|
|
|
|
|
UsageCheckResponse,
|
|
|
|
|
UsageSummary,
|
|
|
|
|
AvailablePlans,
|
|
|
|
|
PlanUpgradeValidation,
|
2025-09-23 22:11:34 +02:00
|
|
|
PlanUpgradeResult,
|
|
|
|
|
SUBSCRIPTION_PLANS,
|
|
|
|
|
ANALYTICS_LEVELS,
|
|
|
|
|
AnalyticsLevel,
|
|
|
|
|
SubscriptionPlanKey,
|
|
|
|
|
PLAN_HIERARCHY,
|
|
|
|
|
ANALYTICS_HIERARCHY
|
2025-09-05 17:49:48 +02:00
|
|
|
} from '../types/subscription';
|
|
|
|
|
|
2025-09-23 22:11:34 +02:00
|
|
|
// Map plan keys to analytics levels based on backend data
|
|
|
|
|
const PLAN_TO_ANALYTICS_LEVEL: Record<SubscriptionPlanKey, AnalyticsLevel> = {
|
|
|
|
|
[SUBSCRIPTION_PLANS.STARTER]: ANALYTICS_LEVELS.BASIC,
|
|
|
|
|
[SUBSCRIPTION_PLANS.PROFESSIONAL]: ANALYTICS_LEVELS.ADVANCED,
|
|
|
|
|
[SUBSCRIPTION_PLANS.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
|
|
|
|
|
|
2025-09-05 17:49:48 +02:00
|
|
|
export class SubscriptionService {
|
2025-10-06 15:27:01 +02:00
|
|
|
private readonly baseUrl = '/tenants';
|
2025-09-05 17:49:48 +02:00
|
|
|
|
|
|
|
|
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.get<SubscriptionLimits>(`${this.baseUrl}/subscriptions/${tenantId}/limits`);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkFeatureAccess(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
featureName: string
|
|
|
|
|
): Promise<FeatureCheckResponse> {
|
|
|
|
|
return apiClient.get<FeatureCheckResponse>(
|
2025-10-06 15:27:01 +02:00
|
|
|
`${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}/check`
|
2025-09-05 17:49:48 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkUsageLimit(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
|
|
|
|
|
requestedAmount?: number
|
|
|
|
|
): Promise<UsageCheckResponse> {
|
|
|
|
|
const queryParams = new URLSearchParams();
|
|
|
|
|
if (requestedAmount !== undefined) {
|
|
|
|
|
queryParams.append('requested_amount', requestedAmount.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const url = queryParams.toString()
|
2025-10-06 15:27:01 +02:00
|
|
|
? `${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/check?${queryParams.toString()}`
|
|
|
|
|
: `${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/check`;
|
2025-09-05 17:49:48 +02:00
|
|
|
|
|
|
|
|
return apiClient.get<UsageCheckResponse>(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async recordUsage(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
|
|
|
|
|
amount: number = 1
|
|
|
|
|
): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
return apiClient.post<{ success: boolean; message: string }>(
|
2025-10-06 15:27:01 +02:00
|
|
|
`${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/record`,
|
2025-09-05 17:49:48 +02:00
|
|
|
{ amount }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCurrentUsage(tenantId: string): Promise<{
|
|
|
|
|
users: number;
|
|
|
|
|
sales_records: number;
|
|
|
|
|
inventory_items: number;
|
|
|
|
|
api_requests_this_hour: number;
|
|
|
|
|
}> {
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/usage/current`);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
2025-09-20 08:59:12 +02:00
|
|
|
|
|
|
|
|
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
|
2025-10-06 15:27:01 +02:00
|
|
|
return apiClient.get<UsageSummary>(`${this.baseUrl}/subscriptions/${tenantId}/usage`);
|
2025-09-20 08:59:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getAvailablePlans(): Promise<AvailablePlans> {
|
2025-09-25 14:30:47 +02:00
|
|
|
return apiClient.get<AvailablePlans>('/plans');
|
2025-09-20 08:59:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async validatePlanUpgrade(tenantId: string, planKey: string): Promise<PlanUpgradeValidation> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get<PlanUpgradeValidation>(`${this.baseUrl}/subscriptions/${tenantId}/validate-upgrade/${planKey}`);
|
2025-09-20 08:59:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async upgradePlan(tenantId: string, planKey: string): Promise<PlanUpgradeResult> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.post<PlanUpgradeResult>(`${this.baseUrl}/subscriptions/${tenantId}/upgrade?new_plan=${planKey}`, {});
|
2025-09-21 13:27:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async canAddLocation(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-location`);
|
2025-09-21 13:27:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async canAddProduct(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-product`);
|
2025-09-21 13:27:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async canAddUser(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-user`);
|
2025-09-21 13:27:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async hasFeature(tenantId: string, featureName: string): Promise<{ has_feature: boolean; feature_value?: any; plan?: string; reason?: string }> {
|
2025-10-07 07:15:07 +02:00
|
|
|
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}`);
|
2025-09-20 08:59:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatPrice(amount: number): string {
|
|
|
|
|
return new Intl.NumberFormat('es-ES', {
|
|
|
|
|
style: 'currency',
|
|
|
|
|
currency: 'EUR',
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
maximumFractionDigits: 2
|
|
|
|
|
}).format(amount);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 22:11:34 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch available subscription plans from the backend
|
|
|
|
|
*/
|
|
|
|
|
async fetchAvailablePlans(): Promise<AvailablePlans> {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
// Return cached data if it's still valid
|
|
|
|
|
if (cachedPlans && lastFetchTime && (now - lastFetchTime) < CACHE_DURATION) {
|
|
|
|
|
return cachedPlans;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-25 14:30:47 +02:00
|
|
|
const plans = await apiClient.get<AvailablePlans>('/plans');
|
2025-09-23 22:11:34 +02:00
|
|
|
cachedPlans = plans;
|
|
|
|
|
lastFetchTime = now;
|
|
|
|
|
return plans;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to fetch subscription plans:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get plan display information
|
|
|
|
|
*/
|
|
|
|
|
async getPlanDisplayInfo(planKey: string) {
|
|
|
|
|
try {
|
|
|
|
|
const plans = await this.fetchAvailablePlans();
|
|
|
|
|
const plan = plans.plans[planKey];
|
|
|
|
|
|
|
|
|
|
if (plan) {
|
|
|
|
|
return {
|
|
|
|
|
name: plan.name,
|
|
|
|
|
color: this.getPlanColor(planKey),
|
|
|
|
|
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_PLANS.STARTER:
|
|
|
|
|
return 'blue';
|
|
|
|
|
case SUBSCRIPTION_PLANS.PROFESSIONAL:
|
|
|
|
|
return 'purple';
|
|
|
|
|
case SUBSCRIPTION_PLANS.ENTERPRISE:
|
|
|
|
|
return 'amber';
|
|
|
|
|
default:
|
|
|
|
|
return 'gray';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a plan meets minimum requirements
|
|
|
|
|
*/
|
|
|
|
|
doesPlanMeetMinimum(plan: SubscriptionPlanKey, minimumRequired: SubscriptionPlanKey): boolean {
|
|
|
|
|
return PLAN_HIERARCHY[plan] >= PLAN_HIERARCHY[minimumRequired];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get analytics level for a plan
|
|
|
|
|
*/
|
|
|
|
|
getAnalyticsLevelForPlan(plan: SubscriptionPlanKey): AnalyticsLevel {
|
|
|
|
|
return PLAN_TO_ANALYTICS_LEVEL[plan] || ANALYTICS_LEVELS.NONE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if analytics level meets minimum requirements
|
|
|
|
|
*/
|
|
|
|
|
doesAnalyticsLevelMeetMinimum(level: AnalyticsLevel, minimumRequired: AnalyticsLevel): boolean {
|
|
|
|
|
return ANALYTICS_HIERARCHY[level] >= ANALYTICS_HIERARCHY[minimumRequired];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get plan features
|
|
|
|
|
*/
|
|
|
|
|
async getPlanFeatures(planKey: string) {
|
|
|
|
|
try {
|
|
|
|
|
const plans = await this.fetchAvailablePlans();
|
|
|
|
|
const plan = plans.plans[planKey];
|
|
|
|
|
|
|
|
|
|
if (plan) {
|
|
|
|
|
return plan.features || {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to get plan features:', error);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a plan has a specific feature
|
|
|
|
|
*/
|
|
|
|
|
async planHasFeature(planKey: string, featureName: string) {
|
|
|
|
|
try {
|
|
|
|
|
const features = await this.getPlanFeatures(planKey);
|
|
|
|
|
return featureName in features;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to check plan feature:', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-09-20 08:59:12 +02:00
|
|
|
}
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const subscriptionService = new SubscriptionService();
|