2025-09-05 17:49:48 +02:00
|
|
|
import { apiClient } from '../client';
|
2025-09-20 08:59:12 +02:00
|
|
|
import {
|
2025-10-15 16:12:49 +02:00
|
|
|
// New types
|
|
|
|
|
SubscriptionTier,
|
|
|
|
|
SUBSCRIPTION_TIERS,
|
|
|
|
|
BillingCycle,
|
|
|
|
|
PlanMetadata,
|
2025-09-20 08:59:12 +02:00
|
|
|
AvailablePlans,
|
2025-10-15 16:12:49 +02:00
|
|
|
UsageSummary,
|
|
|
|
|
FeatureCheckResponse,
|
|
|
|
|
QuotaCheckResponse,
|
2025-09-20 08:59:12 +02:00
|
|
|
PlanUpgradeValidation,
|
2025-09-23 22:11:34 +02:00
|
|
|
PlanUpgradeResult,
|
2025-10-15 16:12:49 +02:00
|
|
|
doesPlanMeetMinimum,
|
|
|
|
|
getPlanColor,
|
|
|
|
|
getYearlyDiscountPercentage,
|
|
|
|
|
PLAN_HIERARCHY,
|
|
|
|
|
|
|
|
|
|
// Analytics levels
|
2025-09-23 22:11:34 +02:00
|
|
|
ANALYTICS_LEVELS,
|
|
|
|
|
AnalyticsLevel,
|
|
|
|
|
ANALYTICS_HIERARCHY
|
2025-09-05 17:49:48 +02:00
|
|
|
} from '../types/subscription';
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
// Map plan tiers to analytics levels based on backend data
|
2025-11-30 09:12:40 +01:00
|
|
|
const TIER_TO_ANALYTICS_LEVEL: Record<SubscriptionTier | string, AnalyticsLevel> = {
|
2025-10-15 16:12:49 +02:00
|
|
|
[SUBSCRIPTION_TIERS.STARTER]: ANALYTICS_LEVELS.BASIC,
|
|
|
|
|
[SUBSCRIPTION_TIERS.PROFESSIONAL]: ANALYTICS_LEVELS.ADVANCED,
|
2025-11-30 09:12:40 +01:00
|
|
|
[SUBSCRIPTION_TIERS.ENTERPRISE]: ANALYTICS_LEVELS.PREDICTIVE,
|
|
|
|
|
'demo': ANALYTICS_LEVELS.ADVANCED, // Treat demo tier same as professional for analytics access
|
2025-09-23 22:11:34 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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-10-15 16:12:49 +02:00
|
|
|
private readonly plansUrl = '/plans';
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// NEW METHODS - Centralized Plans API
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-10-30 21:08:07 +01:00
|
|
|
/**
|
|
|
|
|
* Invalidate cached plan data
|
|
|
|
|
* Call this when subscription changes to ensure fresh data on next fetch
|
|
|
|
|
*/
|
|
|
|
|
invalidateCache(): void {
|
|
|
|
|
cachedPlans = null;
|
|
|
|
|
lastFetchTime = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch available subscription plans with complete metadata
|
|
|
|
|
* Uses cached data if available and fresh (5 min cache)
|
|
|
|
|
*/
|
|
|
|
|
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 {
|
|
|
|
|
const plans = await apiClient.get<AvailablePlans>(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<PlanMetadata | null> {
|
|
|
|
|
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<string[]> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const features = await this.getPlanFeatures(tier);
|
|
|
|
|
return features.includes(featureName);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to check feature availability:', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-05 17:49:48 +02:00
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
/**
|
|
|
|
|
* Get plan comparison data for pricing page
|
|
|
|
|
*/
|
|
|
|
|
async getPlanComparison(): Promise<{
|
|
|
|
|
tiers: SubscriptionTier[];
|
|
|
|
|
metadata: Record<SubscriptionTier, PlanMetadata>;
|
|
|
|
|
}> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
/**
|
|
|
|
|
* 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<UsageSummary> {
|
|
|
|
|
return apiClient.get<UsageSummary>(`${this.baseUrl}/subscriptions/${tenantId}/usage`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if tenant has access to a specific feature
|
|
|
|
|
*/
|
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
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
/**
|
|
|
|
|
* Check if tenant can perform an action within quota limits
|
|
|
|
|
*/
|
2025-10-27 16:33:26 +01:00
|
|
|
async checkQuotaLimit(
|
2025-10-06 15:27:01 +02:00
|
|
|
tenantId: string,
|
2025-10-15 16:12:49 +02:00
|
|
|
quotaType: string,
|
2025-09-05 17:49:48 +02:00
|
|
|
requestedAmount?: number
|
2025-10-15 16:12:49 +02:00
|
|
|
): Promise<QuotaCheckResponse> {
|
2025-10-27 16:33:26 +01:00
|
|
|
// 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}`);
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-27 16:33:26 +01:00
|
|
|
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 || ''
|
|
|
|
|
};
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get plan display information
|
|
|
|
|
*/
|
|
|
|
|
async getPlanDisplayInfo(planKey: string) {
|
|
|
|
|
try {
|
|
|
|
|
const plans = await this.fetchAvailablePlans();
|
2025-10-15 16:12:49 +02:00
|
|
|
const plan = plans.plans[planKey as SubscriptionTier];
|
|
|
|
|
|
2025-09-23 22:11:34 +02:00
|
|
|
if (plan) {
|
|
|
|
|
return {
|
|
|
|
|
name: plan.name,
|
2025-10-15 16:12:49 +02:00
|
|
|
color: this.getPlanColor(planKey as SubscriptionTier),
|
2025-09-23 22:11:34 +02:00
|
|
|
description: plan.description,
|
|
|
|
|
monthlyPrice: plan.monthly_price
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-10-15 16:12:49 +02:00
|
|
|
|
2025-09-23 22:11:34 +02:00
|
|
|
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) {
|
2025-10-15 16:12:49 +02:00
|
|
|
case SUBSCRIPTION_TIERS.STARTER:
|
2025-09-23 22:11:34 +02:00
|
|
|
return 'blue';
|
2025-10-15 16:12:49 +02:00
|
|
|
case SUBSCRIPTION_TIERS.PROFESSIONAL:
|
2025-09-23 22:11:34 +02:00
|
|
|
return 'purple';
|
2025-10-15 16:12:49 +02:00
|
|
|
case SUBSCRIPTION_TIERS.ENTERPRISE:
|
2025-09-23 22:11:34 +02:00
|
|
|
return 'amber';
|
|
|
|
|
default:
|
|
|
|
|
return 'gray';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-15 16:12:49 +02:00
|
|
|
* Get analytics level for a plan tier
|
2025-09-23 22:11:34 +02:00
|
|
|
*/
|
2025-10-15 16:12:49 +02:00
|
|
|
getAnalyticsLevelForTier(tier: SubscriptionTier): AnalyticsLevel {
|
|
|
|
|
return TIER_TO_ANALYTICS_LEVEL[tier] || ANALYTICS_LEVELS.NONE;
|
2025-09-23 22:11:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-15 16:12:49 +02:00
|
|
|
* Get analytics level for a plan (alias for getAnalyticsLevelForTier)
|
|
|
|
|
* @deprecated Use getAnalyticsLevelForTier instead
|
2025-09-23 22:11:34 +02:00
|
|
|
*/
|
2025-10-15 16:12:49 +02:00
|
|
|
getAnalyticsLevelForPlan(tier: SubscriptionTier): AnalyticsLevel {
|
|
|
|
|
return this.getAnalyticsLevelForTier(tier);
|
2025-09-23 22:11:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if analytics level meets minimum requirements
|
|
|
|
|
*/
|
|
|
|
|
doesAnalyticsLevelMeetMinimum(level: AnalyticsLevel, minimumRequired: AnalyticsLevel): boolean {
|
|
|
|
|
return ANALYTICS_HIERARCHY[level] >= ANALYTICS_HIERARCHY[minimumRequired];
|
|
|
|
|
}
|
2025-10-16 07:28:04 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2025-12-05 20:07:01 +01:00
|
|
|
billing_cycle?: string;
|
|
|
|
|
next_billing_date?: string;
|
2025-10-16 07:28:04 +02:00
|
|
|
}> {
|
|
|
|
|
return apiClient.get(`/subscriptions/${tenantId}/status`);
|
|
|
|
|
}
|
2025-10-31 11:54:19 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get invoice history for a tenant
|
|
|
|
|
*/
|
|
|
|
|
async getInvoices(tenantId: string): Promise<Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
date: string;
|
|
|
|
|
amount: number;
|
|
|
|
|
currency: string;
|
|
|
|
|
status: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
invoice_pdf: string | null;
|
|
|
|
|
hosted_invoice_url: string | null;
|
|
|
|
|
}>> {
|
|
|
|
|
return apiClient.get(`/subscriptions/${tenantId}/invoices`);
|
|
|
|
|
}
|
2025-11-19 21:01:06 +01:00
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 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,
|
2025-12-05 20:07:01 +01:00
|
|
|
billing_cycle: (status.billing_cycle as 'monthly' | 'yearly') || 'monthly',
|
2025-11-19 21:01:06 +01:00
|
|
|
monthly_price: currentPlan?.monthly_price || 0,
|
|
|
|
|
yearly_price: currentPlan?.yearly_price || 0,
|
2025-12-05 20:07:01 +01:00
|
|
|
renewal_date: status.next_billing_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
2025-11-19 21:01:06 +01:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-05 17:49:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-27 16:33:26 +01:00
|
|
|
export const subscriptionService = new SubscriptionService();
|