Improve subcription support
This commit is contained in:
@@ -218,4 +218,5 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
export { ApiClient };
|
||||
export const apiClient = new ApiClient();
|
||||
@@ -1,5 +1,6 @@
|
||||
// Export API client and types
|
||||
export * from './client';
|
||||
export { ApiClient } from './client';
|
||||
|
||||
// Export all services
|
||||
export * from './auth.service';
|
||||
@@ -14,6 +15,7 @@ export * from './pos.service';
|
||||
export * from './data.service';
|
||||
export * from './training.service';
|
||||
export * from './notification.service';
|
||||
export * from './subscription.service';
|
||||
|
||||
// Service instances for easy importing
|
||||
export { authService } from './auth.service';
|
||||
@@ -28,6 +30,7 @@ export { posService } from './pos.service';
|
||||
export { dataService } from './data.service';
|
||||
export { trainingService } from './training.service';
|
||||
export { notificationService } from './notification.service';
|
||||
export { subscriptionService } from './subscription.service';
|
||||
|
||||
// API client instance
|
||||
export { apiClient } from './client';
|
||||
481
frontend/src/services/api/subscription.service.ts
Normal file
481
frontend/src/services/api/subscription.service.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* Subscription Service
|
||||
* Handles API calls for subscription management, billing, and plan limits
|
||||
*/
|
||||
|
||||
import { ApiClient } from './client';
|
||||
import { isMockMode, getMockSubscription } from '../../config/mock.config';
|
||||
|
||||
export interface SubscriptionLimits {
|
||||
plan: string;
|
||||
max_users: number;
|
||||
max_locations: number;
|
||||
max_products: number;
|
||||
features: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UsageSummary {
|
||||
plan: string;
|
||||
monthly_price: number;
|
||||
status: string;
|
||||
usage: {
|
||||
users: {
|
||||
current: number;
|
||||
limit: number;
|
||||
unlimited: boolean;
|
||||
usage_percentage: number;
|
||||
};
|
||||
locations: {
|
||||
current: number;
|
||||
limit: number;
|
||||
unlimited: boolean;
|
||||
usage_percentage: number;
|
||||
};
|
||||
products: {
|
||||
current: number;
|
||||
limit: number;
|
||||
unlimited: boolean;
|
||||
usage_percentage: number;
|
||||
};
|
||||
};
|
||||
features: Record<string, any>;
|
||||
next_billing_date?: string;
|
||||
trial_ends_at?: string;
|
||||
}
|
||||
|
||||
export interface LimitCheckResult {
|
||||
can_add: boolean;
|
||||
current_count?: number;
|
||||
max_allowed?: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface FeatureCheckResult {
|
||||
has_feature: boolean;
|
||||
feature_value?: any;
|
||||
plan: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface PlanUpgradeValidation {
|
||||
can_upgrade: boolean;
|
||||
current_plan?: string;
|
||||
new_plan?: string;
|
||||
price_change?: number;
|
||||
new_features?: Record<string, any>;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface AvailablePlan {
|
||||
name: string;
|
||||
description: string;
|
||||
monthly_price: number;
|
||||
max_users: number;
|
||||
max_locations: number;
|
||||
max_products: number;
|
||||
features: Record<string, any>;
|
||||
trial_available: boolean;
|
||||
popular?: boolean;
|
||||
contact_sales?: boolean;
|
||||
}
|
||||
|
||||
export interface AvailablePlans {
|
||||
plans: Record<string, AvailablePlan>;
|
||||
}
|
||||
|
||||
export interface BillingHistoryItem {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
status: 'paid' | 'pending' | 'failed';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SubscriptionData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
plan: string;
|
||||
status: string;
|
||||
monthly_price: number;
|
||||
currency: string;
|
||||
billing_cycle: string;
|
||||
current_period_start: string;
|
||||
current_period_end: string;
|
||||
next_billing_date: string;
|
||||
trial_ends_at?: string | null;
|
||||
canceled_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
max_users: number;
|
||||
max_locations: number;
|
||||
max_products: number;
|
||||
features: Record<string, any>;
|
||||
usage: {
|
||||
users: number;
|
||||
locations: number;
|
||||
products: number;
|
||||
storage_gb: number;
|
||||
api_calls_month: number;
|
||||
reports_generated: number;
|
||||
};
|
||||
billing_history: BillingHistoryItem[];
|
||||
}
|
||||
|
||||
class SubscriptionService {
|
||||
private apiClient: ApiClient;
|
||||
|
||||
constructor() {
|
||||
this.apiClient = new ApiClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current subscription limits for a tenant
|
||||
*/
|
||||
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
|
||||
if (isMockMode()) {
|
||||
const mockSub = getMockSubscription();
|
||||
return {
|
||||
plan: mockSub.plan,
|
||||
max_users: mockSub.max_users,
|
||||
max_locations: mockSub.max_locations,
|
||||
max_products: mockSub.max_products,
|
||||
features: mockSub.features
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/limits`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage summary vs limits for a tenant
|
||||
*/
|
||||
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
|
||||
if (isMockMode()) {
|
||||
console.log('🧪 Mock mode: Returning usage summary for tenant:', tenantId);
|
||||
const mockSub = getMockSubscription();
|
||||
return {
|
||||
plan: mockSub.plan,
|
||||
monthly_price: mockSub.monthly_price,
|
||||
status: mockSub.status,
|
||||
usage: {
|
||||
users: {
|
||||
current: mockSub.usage.users,
|
||||
limit: mockSub.max_users,
|
||||
unlimited: mockSub.max_users === -1,
|
||||
usage_percentage: mockSub.max_users === -1 ? 0 : Math.round((mockSub.usage.users / mockSub.max_users) * 100)
|
||||
},
|
||||
locations: {
|
||||
current: mockSub.usage.locations,
|
||||
limit: mockSub.max_locations,
|
||||
unlimited: mockSub.max_locations === -1,
|
||||
usage_percentage: mockSub.max_locations === -1 ? 0 : Math.round((mockSub.usage.locations / mockSub.max_locations) * 100)
|
||||
},
|
||||
products: {
|
||||
current: mockSub.usage.products,
|
||||
limit: mockSub.max_products,
|
||||
unlimited: mockSub.max_products === -1,
|
||||
usage_percentage: mockSub.max_products === -1 ? 0 : Math.round((mockSub.usage.products / mockSub.max_products) * 100)
|
||||
}
|
||||
},
|
||||
features: mockSub.features,
|
||||
next_billing_date: mockSub.next_billing_date,
|
||||
trial_ends_at: mockSub.trial_ends_at
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/usage`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant can add another location
|
||||
*/
|
||||
async canAddLocation(tenantId: string): Promise<LimitCheckResult> {
|
||||
if (isMockMode()) {
|
||||
const mockSub = getMockSubscription();
|
||||
const canAdd = mockSub.max_locations === -1 || mockSub.usage.locations < mockSub.max_locations;
|
||||
return {
|
||||
can_add: canAdd,
|
||||
current_count: mockSub.usage.locations,
|
||||
max_allowed: mockSub.max_locations,
|
||||
reason: canAdd ? 'Can add more locations' : 'Location limit reached. Upgrade plan to add more locations.'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-location`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant can add another product
|
||||
*/
|
||||
async canAddProduct(tenantId: string): Promise<LimitCheckResult> {
|
||||
if (isMockMode()) {
|
||||
const mockSub = getMockSubscription();
|
||||
const canAdd = mockSub.max_products === -1 || mockSub.usage.products < mockSub.max_products;
|
||||
return {
|
||||
can_add: canAdd,
|
||||
current_count: mockSub.usage.products,
|
||||
max_allowed: mockSub.max_products,
|
||||
reason: canAdd ? 'Can add more products' : 'Product limit reached. Upgrade plan to add more products.'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-product`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant can add another user/member
|
||||
*/
|
||||
async canAddUser(tenantId: string): Promise<LimitCheckResult> {
|
||||
if (isMockMode()) {
|
||||
const mockSub = getMockSubscription();
|
||||
const canAdd = mockSub.max_users === -1 || mockSub.usage.users < mockSub.max_users;
|
||||
return {
|
||||
can_add: canAdd,
|
||||
current_count: mockSub.usage.users,
|
||||
max_allowed: mockSub.max_users,
|
||||
reason: canAdd ? 'Can add more users' : 'User limit reached. Upgrade plan to add more users.'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/can-add-user`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant has access to a specific feature
|
||||
*/
|
||||
async hasFeature(tenantId: string, feature: string): Promise<FeatureCheckResult> {
|
||||
if (isMockMode()) {
|
||||
const mockSub = getMockSubscription();
|
||||
const hasFeature = feature in mockSub.features;
|
||||
return {
|
||||
has_feature: hasFeature,
|
||||
feature_value: hasFeature ? mockSub.features[feature] : null,
|
||||
plan: mockSub.plan,
|
||||
reason: hasFeature ? `Feature ${feature} is available in ${mockSub.plan} plan` : `Feature ${feature} is not available in ${mockSub.plan} plan`
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/features/${feature}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if tenant can upgrade to a new plan
|
||||
*/
|
||||
async validatePlanUpgrade(tenantId: string, newPlan: string): Promise<PlanUpgradeValidation> {
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/validate-upgrade/${newPlan}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade subscription plan for a tenant
|
||||
*/
|
||||
async upgradePlan(tenantId: string, newPlan: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
validation: PlanUpgradeValidation;
|
||||
}> {
|
||||
const response = await this.apiClient.post(`/subscriptions/${tenantId}/upgrade`, null, {
|
||||
params: { new_plan: newPlan }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full subscription data including billing history for admin@bakery.com
|
||||
*/
|
||||
async getSubscriptionData(tenantId: string): Promise<SubscriptionData> {
|
||||
if (isMockMode()) {
|
||||
return getMockSubscription();
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/details`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get billing history for a subscription
|
||||
*/
|
||||
async getBillingHistory(tenantId: string): Promise<BillingHistoryItem[]> {
|
||||
if (isMockMode()) {
|
||||
return getMockSubscription().billing_history;
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get(`/subscriptions/${tenantId}/billing-history`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available subscription plans with features and pricing
|
||||
*/
|
||||
async getAvailablePlans(): Promise<AvailablePlans> {
|
||||
if (isMockMode()) {
|
||||
return {
|
||||
plans: {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
description: 'Perfecto para panaderías pequeñas que están comenzando',
|
||||
monthly_price: 49.0,
|
||||
max_users: 5,
|
||||
max_locations: 1,
|
||||
max_products: 50,
|
||||
features: {
|
||||
inventory_management: 'basic',
|
||||
demand_prediction: 'basic',
|
||||
production_reports: 'basic',
|
||||
analytics: 'basic',
|
||||
support: 'email',
|
||||
trial_days: 14,
|
||||
locations: '1_location'
|
||||
},
|
||||
trial_available: true
|
||||
},
|
||||
professional: {
|
||||
name: 'Professional',
|
||||
description: 'Para panaderías en crecimiento que necesitan más control',
|
||||
monthly_price: 129.0,
|
||||
max_users: 15,
|
||||
max_locations: 2,
|
||||
max_products: -1,
|
||||
features: {
|
||||
inventory_management: 'advanced',
|
||||
demand_prediction: 'ai_92_percent',
|
||||
production_management: 'complete',
|
||||
pos_integrated: true,
|
||||
logistics: 'basic',
|
||||
analytics: 'advanced',
|
||||
support: 'priority_24_7',
|
||||
trial_days: 14,
|
||||
locations: '1_2_locations'
|
||||
},
|
||||
trial_available: true,
|
||||
popular: true
|
||||
},
|
||||
enterprise: {
|
||||
name: 'Enterprise',
|
||||
description: 'Para cadenas de panaderías con necesidades avanzadas',
|
||||
monthly_price: 399.0,
|
||||
max_users: -1,
|
||||
max_locations: -1,
|
||||
max_products: -1,
|
||||
features: {
|
||||
inventory_management: 'multi_location',
|
||||
demand_prediction: 'ai_personalized',
|
||||
production_optimization: 'capacity',
|
||||
erp_integration: true,
|
||||
logistics: 'advanced',
|
||||
analytics: 'predictive',
|
||||
api_access: 'personalized',
|
||||
account_manager: true,
|
||||
demo: 'personalized',
|
||||
locations: 'unlimited_obradores'
|
||||
},
|
||||
trial_available: false,
|
||||
contact_sales: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.apiClient.get('/plans/available');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if a feature is enabled for current tenant
|
||||
*/
|
||||
async isFeatureEnabled(tenantId: string, feature: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.hasFeature(tenantId, feature);
|
||||
return result.has_feature;
|
||||
} catch (error) {
|
||||
console.error(`Error checking feature ${feature}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get feature level (basic, advanced, etc.)
|
||||
*/
|
||||
async getFeatureLevel(tenantId: string, feature: string): Promise<string | null> {
|
||||
try {
|
||||
const result = await this.hasFeature(tenantId, feature);
|
||||
return result.feature_value || null;
|
||||
} catch (error) {
|
||||
console.error(`Error getting feature level for ${feature}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if usage is approaching limits
|
||||
*/
|
||||
async isUsageNearLimit(tenantId: string, threshold: number = 80): Promise<{
|
||||
users: boolean;
|
||||
locations: boolean;
|
||||
products: boolean;
|
||||
}> {
|
||||
try {
|
||||
const usage = await this.getUsageSummary(tenantId);
|
||||
|
||||
return {
|
||||
users: !usage.usage.users.unlimited && usage.usage.users.usage_percentage >= threshold,
|
||||
locations: !usage.usage.locations.unlimited && usage.usage.locations.usage_percentage >= threshold,
|
||||
products: !usage.usage.products.unlimited && usage.usage.products.usage_percentage >= threshold,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking usage limits:', error);
|
||||
return { users: false, locations: false, products: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to format pricing for display
|
||||
*/
|
||||
formatPrice(price: number, currency: string = 'EUR'): string {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get plan display information
|
||||
*/
|
||||
getPlanDisplayInfo(plan: string): {
|
||||
name: string;
|
||||
color: string;
|
||||
badge?: string;
|
||||
} {
|
||||
const planInfo = {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
color: 'blue',
|
||||
},
|
||||
professional: {
|
||||
name: 'Professional',
|
||||
color: 'purple',
|
||||
badge: 'Más Popular'
|
||||
},
|
||||
enterprise: {
|
||||
name: 'Enterprise',
|
||||
color: 'gold',
|
||||
}
|
||||
};
|
||||
|
||||
return planInfo[plan as keyof typeof planInfo] || {
|
||||
name: plan,
|
||||
color: 'gray'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const subscriptionService = new SubscriptionService();
|
||||
export default subscriptionService;
|
||||
Reference in New Issue
Block a user