Add role-based filtering and imporve code
This commit is contained in:
@@ -4,11 +4,9 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { subscriptionService } from '../services/subscription';
|
||||
import {
|
||||
SUBSCRIPTION_PLANS,
|
||||
ANALYTICS_LEVELS,
|
||||
AnalyticsLevel,
|
||||
SubscriptionPlanKey
|
||||
import {
|
||||
SUBSCRIPTION_TIERS,
|
||||
SubscriptionTier
|
||||
} from '../types/subscription';
|
||||
import { useCurrentTenant } from '../../stores';
|
||||
import { useAuthUser } from '../../stores/auth.store';
|
||||
@@ -28,7 +26,7 @@ export interface SubscriptionLimits {
|
||||
|
||||
export interface SubscriptionInfo {
|
||||
plan: string;
|
||||
status: 'active' | 'inactive' | 'past_due' | 'cancelled';
|
||||
status: 'active' | 'inactive' | 'past_due' | 'cancelled' | 'trialing';
|
||||
features: Record<string, any>;
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
@@ -101,14 +99,14 @@ export const useSubscription = () => {
|
||||
const getAnalyticsAccess = useCallback((): { hasAccess: boolean; level: string; reason?: string } => {
|
||||
const { plan } = subscriptionInfo;
|
||||
|
||||
// Convert plan to typed plan key if it matches our known plans
|
||||
let planKey: keyof typeof SUBSCRIPTION_PLANS | undefined;
|
||||
if (plan === SUBSCRIPTION_PLANS.STARTER) planKey = SUBSCRIPTION_PLANS.STARTER;
|
||||
else if (plan === SUBSCRIPTION_PLANS.PROFESSIONAL) planKey = SUBSCRIPTION_PLANS.PROFESSIONAL;
|
||||
else if (plan === SUBSCRIPTION_PLANS.ENTERPRISE) planKey = SUBSCRIPTION_PLANS.ENTERPRISE;
|
||||
// Convert plan string to typed SubscriptionTier
|
||||
let tierKey: SubscriptionTier | undefined;
|
||||
if (plan === SUBSCRIPTION_TIERS.STARTER) tierKey = SUBSCRIPTION_TIERS.STARTER;
|
||||
else if (plan === SUBSCRIPTION_TIERS.PROFESSIONAL) tierKey = SUBSCRIPTION_TIERS.PROFESSIONAL;
|
||||
else if (plan === SUBSCRIPTION_TIERS.ENTERPRISE) tierKey = SUBSCRIPTION_TIERS.ENTERPRISE;
|
||||
|
||||
if (planKey) {
|
||||
const analyticsLevel = subscriptionService.getAnalyticsLevelForPlan(planKey);
|
||||
if (tierKey) {
|
||||
const analyticsLevel = subscriptionService.getAnalyticsLevelForTier(tierKey);
|
||||
return { hasAccess: true, level: analyticsLevel };
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,16 @@ export type {
|
||||
AvailablePlans,
|
||||
Plan,
|
||||
PlanUpgradeValidation,
|
||||
PlanUpgradeResult
|
||||
PlanUpgradeResult,
|
||||
SubscriptionTier,
|
||||
BillingCycle,
|
||||
PlanMetadata
|
||||
} from './types/subscription';
|
||||
|
||||
export {
|
||||
SUBSCRIPTION_TIERS,
|
||||
BILLING_CYCLES,
|
||||
ANALYTICS_LEVELS
|
||||
} from './types/subscription';
|
||||
|
||||
// Types - Sales
|
||||
|
||||
108
frontend/src/api/services/nominatim.ts
Normal file
108
frontend/src/api/services/nominatim.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Nominatim Geocoding API Service
|
||||
* Provides address search and autocomplete functionality
|
||||
*/
|
||||
|
||||
import apiClient from '../client';
|
||||
|
||||
export interface NominatimResult {
|
||||
place_id: number;
|
||||
lat: string;
|
||||
lon: string;
|
||||
display_name: string;
|
||||
address: {
|
||||
road?: string;
|
||||
house_number?: string;
|
||||
city?: string;
|
||||
town?: string;
|
||||
village?: string;
|
||||
municipality?: string;
|
||||
postcode?: string;
|
||||
country?: string;
|
||||
};
|
||||
boundingbox: [string, string, string, string];
|
||||
}
|
||||
|
||||
export interface NominatimSearchParams {
|
||||
q: string;
|
||||
format?: 'json';
|
||||
addressdetails?: 1 | 0;
|
||||
limit?: number;
|
||||
countrycodes?: string;
|
||||
}
|
||||
|
||||
class NominatimService {
|
||||
private baseUrl = '/api/v1/nominatim';
|
||||
|
||||
/**
|
||||
* Search for addresses matching a query
|
||||
*/
|
||||
async searchAddress(query: string, limit: number = 5): Promise<NominatimResult[]> {
|
||||
if (!query || query.length < 3) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<NominatimResult[]>(`${this.baseUrl}/search`, {
|
||||
params: {
|
||||
q: query,
|
||||
format: 'json',
|
||||
addressdetails: 1,
|
||||
limit,
|
||||
countrycodes: 'es', // Spain only
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Address search failed:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Nominatim result for display
|
||||
*/
|
||||
formatAddress(result: NominatimResult): string {
|
||||
return result.display_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structured address components
|
||||
*/
|
||||
parseAddress(result: NominatimResult) {
|
||||
const { address } = result;
|
||||
|
||||
return {
|
||||
street: address.road
|
||||
? `${address.road}${address.house_number ? ' ' + address.house_number : ''}`
|
||||
: '',
|
||||
city: address.city || address.town || address.village || address.municipality || '',
|
||||
postalCode: address.postcode || '',
|
||||
latitude: parseFloat(result.lat),
|
||||
longitude: parseFloat(result.lon),
|
||||
displayName: result.display_name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode a structured address to coordinates
|
||||
*/
|
||||
async geocodeAddress(
|
||||
street: string,
|
||||
city: string,
|
||||
postalCode?: string
|
||||
): Promise<NominatimResult | null> {
|
||||
const parts = [street, city];
|
||||
if (postalCode) parts.push(postalCode);
|
||||
parts.push('Spain');
|
||||
|
||||
const query = parts.join(', ');
|
||||
const results = await this.searchAddress(query, 1);
|
||||
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
export const nominatimService = new NominatimService();
|
||||
export default nominatimService;
|
||||
@@ -1,25 +1,32 @@
|
||||
import { apiClient } from '../client';
|
||||
import {
|
||||
SubscriptionLimits,
|
||||
FeatureCheckResponse,
|
||||
UsageCheckResponse,
|
||||
UsageSummary,
|
||||
// New types
|
||||
SubscriptionTier,
|
||||
SUBSCRIPTION_TIERS,
|
||||
BillingCycle,
|
||||
PlanMetadata,
|
||||
AvailablePlans,
|
||||
UsageSummary,
|
||||
FeatureCheckResponse,
|
||||
QuotaCheckResponse,
|
||||
PlanUpgradeValidation,
|
||||
PlanUpgradeResult,
|
||||
SUBSCRIPTION_PLANS,
|
||||
doesPlanMeetMinimum,
|
||||
getPlanColor,
|
||||
getYearlyDiscountPercentage,
|
||||
PLAN_HIERARCHY,
|
||||
|
||||
// Analytics levels
|
||||
ANALYTICS_LEVELS,
|
||||
AnalyticsLevel,
|
||||
SubscriptionPlanKey,
|
||||
PLAN_HIERARCHY,
|
||||
ANALYTICS_HIERARCHY
|
||||
} from '../types/subscription';
|
||||
|
||||
// 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
|
||||
// Map plan tiers to analytics levels based on backend data
|
||||
const TIER_TO_ANALYTICS_LEVEL: Record<SubscriptionTier, AnalyticsLevel> = {
|
||||
[SUBSCRIPTION_TIERS.STARTER]: ANALYTICS_LEVELS.BASIC,
|
||||
[SUBSCRIPTION_TIERS.PROFESSIONAL]: ANALYTICS_LEVELS.ADVANCED,
|
||||
[SUBSCRIPTION_TIERS.ENTERPRISE]: ANALYTICS_LEVELS.PREDICTIVE
|
||||
};
|
||||
|
||||
// Cache for available plans
|
||||
@@ -29,11 +36,145 @@ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export class SubscriptionService {
|
||||
private readonly baseUrl = '/tenants';
|
||||
private readonly plansUrl = '/plans';
|
||||
|
||||
async getSubscriptionLimits(tenantId: string): Promise<SubscriptionLimits> {
|
||||
return apiClient.get<SubscriptionLimits>(`${this.baseUrl}/subscriptions/${tenantId}/limits`);
|
||||
// ============================================================================
|
||||
// NEW METHODS - Centralized Plans API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async checkFeatureAccess(
|
||||
tenantId: string,
|
||||
featureName: string
|
||||
@@ -43,49 +184,24 @@ export class SubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
async checkUsageLimit(
|
||||
/**
|
||||
* Check if tenant can perform an action within quota limits
|
||||
*/
|
||||
async checkQuotaLimit(
|
||||
tenantId: string,
|
||||
resourceType: 'users' | 'sales_records' | 'inventory_items' | 'api_requests',
|
||||
quotaType: string,
|
||||
requestedAmount?: number
|
||||
): Promise<UsageCheckResponse> {
|
||||
): Promise<QuotaCheckResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (requestedAmount !== undefined) {
|
||||
queryParams.append('requested_amount', requestedAmount.toString());
|
||||
}
|
||||
|
||||
const url = queryParams.toString()
|
||||
? `${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/check?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/check`;
|
||||
? `${this.baseUrl}/subscriptions/${tenantId}/quotas/${quotaType}/check?${queryParams.toString()}`
|
||||
: `${this.baseUrl}/subscriptions/${tenantId}/quotas/${quotaType}/check`;
|
||||
|
||||
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 }>(
|
||||
`${this.baseUrl}/subscriptions/${tenantId}/usage/${resourceType}/record`,
|
||||
{ amount }
|
||||
);
|
||||
}
|
||||
|
||||
async getCurrentUsage(tenantId: string): Promise<{
|
||||
users: number;
|
||||
sales_records: number;
|
||||
inventory_items: number;
|
||||
api_requests_this_hour: number;
|
||||
}> {
|
||||
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/usage/current`);
|
||||
}
|
||||
|
||||
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
|
||||
return apiClient.get<UsageSummary>(`${this.baseUrl}/subscriptions/${tenantId}/usage`);
|
||||
}
|
||||
|
||||
async getAvailablePlans(): Promise<AvailablePlans> {
|
||||
return apiClient.get<AvailablePlans>('/plans');
|
||||
return apiClient.get<QuotaCheckResponse>(url);
|
||||
}
|
||||
|
||||
async validatePlanUpgrade(tenantId: string, planKey: string): Promise<PlanUpgradeValidation> {
|
||||
@@ -121,27 +237,6 @@ export class SubscriptionService {
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const plans = await apiClient.get<AvailablePlans>('/plans');
|
||||
cachedPlans = plans;
|
||||
lastFetchTime = now;
|
||||
return plans;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch subscription plans:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan display information
|
||||
@@ -149,17 +244,17 @@ export class SubscriptionService {
|
||||
async getPlanDisplayInfo(planKey: string) {
|
||||
try {
|
||||
const plans = await this.fetchAvailablePlans();
|
||||
const plan = plans.plans[planKey];
|
||||
|
||||
const plan = plans.plans[planKey as SubscriptionTier];
|
||||
|
||||
if (plan) {
|
||||
return {
|
||||
name: plan.name,
|
||||
color: this.getPlanColor(planKey),
|
||||
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);
|
||||
@@ -172,11 +267,11 @@ export class SubscriptionService {
|
||||
*/
|
||||
getPlanColor(planKey: string): string {
|
||||
switch (planKey) {
|
||||
case SUBSCRIPTION_PLANS.STARTER:
|
||||
case SUBSCRIPTION_TIERS.STARTER:
|
||||
return 'blue';
|
||||
case SUBSCRIPTION_PLANS.PROFESSIONAL:
|
||||
case SUBSCRIPTION_TIERS.PROFESSIONAL:
|
||||
return 'purple';
|
||||
case SUBSCRIPTION_PLANS.ENTERPRISE:
|
||||
case SUBSCRIPTION_TIERS.ENTERPRISE:
|
||||
return 'amber';
|
||||
default:
|
||||
return 'gray';
|
||||
@@ -184,17 +279,18 @@ export class SubscriptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a plan meets minimum requirements
|
||||
* Get analytics level for a plan tier
|
||||
*/
|
||||
doesPlanMeetMinimum(plan: SubscriptionPlanKey, minimumRequired: SubscriptionPlanKey): boolean {
|
||||
return PLAN_HIERARCHY[plan] >= PLAN_HIERARCHY[minimumRequired];
|
||||
getAnalyticsLevelForTier(tier: SubscriptionTier): AnalyticsLevel {
|
||||
return TIER_TO_ANALYTICS_LEVEL[tier] || ANALYTICS_LEVELS.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics level for a plan
|
||||
* Get analytics level for a plan (alias for getAnalyticsLevelForTier)
|
||||
* @deprecated Use getAnalyticsLevelForTier instead
|
||||
*/
|
||||
getAnalyticsLevelForPlan(plan: SubscriptionPlanKey): AnalyticsLevel {
|
||||
return PLAN_TO_ANALYTICS_LEVEL[plan] || ANALYTICS_LEVELS.NONE;
|
||||
getAnalyticsLevelForPlan(tier: SubscriptionTier): AnalyticsLevel {
|
||||
return this.getAnalyticsLevelForTier(tier);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,38 +299,6 @@ export class SubscriptionService {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const subscriptionService = new SubscriptionService();
|
||||
@@ -1,21 +1,216 @@
|
||||
/**
|
||||
* Subscription API Types - Mirror backend schemas
|
||||
* Subscription API Types - Mirror backend centralized plans configuration
|
||||
* Source: /shared/subscription/plans.py
|
||||
*/
|
||||
|
||||
export interface SubscriptionLimits {
|
||||
max_users: number;
|
||||
max_sales_records: number;
|
||||
max_inventory_items: number;
|
||||
max_api_requests_per_hour: number;
|
||||
features_enabled: string[];
|
||||
current_usage: {
|
||||
users: number;
|
||||
sales_records: number;
|
||||
inventory_items: number;
|
||||
api_requests_this_hour: number;
|
||||
// ============================================================================
|
||||
// SUBSCRIPTION PLAN ENUMS
|
||||
// ============================================================================
|
||||
|
||||
export const SUBSCRIPTION_TIERS = {
|
||||
STARTER: 'starter',
|
||||
PROFESSIONAL: 'professional',
|
||||
ENTERPRISE: 'enterprise'
|
||||
} as const;
|
||||
|
||||
export type SubscriptionTier = typeof SUBSCRIPTION_TIERS[keyof typeof SUBSCRIPTION_TIERS];
|
||||
|
||||
export const BILLING_CYCLES = {
|
||||
MONTHLY: 'monthly',
|
||||
YEARLY: 'yearly'
|
||||
} as const;
|
||||
|
||||
export type BillingCycle = typeof BILLING_CYCLES[keyof typeof BILLING_CYCLES];
|
||||
|
||||
// ============================================================================
|
||||
// QUOTA LIMITS
|
||||
// ============================================================================
|
||||
|
||||
export interface QuotaLimits {
|
||||
// Team & Organization
|
||||
max_users?: number | null; // null = unlimited
|
||||
max_locations?: number | null;
|
||||
|
||||
// Product & Inventory
|
||||
max_products?: number | null;
|
||||
max_recipes?: number | null;
|
||||
max_suppliers?: number | null;
|
||||
|
||||
// ML & Analytics (Daily)
|
||||
training_jobs_per_day?: number | null;
|
||||
forecast_generation_per_day?: number | null;
|
||||
|
||||
// Data Limits
|
||||
dataset_size_rows?: number | null;
|
||||
forecast_horizon_days?: number | null;
|
||||
historical_data_access_days?: number | null;
|
||||
|
||||
// Import/Export
|
||||
bulk_import_rows?: number | null;
|
||||
bulk_export_rows?: number | null;
|
||||
|
||||
// Integrations
|
||||
pos_sync_interval_minutes?: number | null;
|
||||
api_calls_per_hour?: number | null;
|
||||
webhook_endpoints?: number | null;
|
||||
|
||||
// Storage
|
||||
file_storage_gb?: number | null;
|
||||
report_retention_days?: number | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLAN FEATURES
|
||||
// ============================================================================
|
||||
|
||||
export interface PlanFeatures {
|
||||
// Core features (all tiers)
|
||||
inventory_management: boolean;
|
||||
sales_tracking: boolean;
|
||||
basic_recipes: boolean;
|
||||
production_planning: boolean;
|
||||
basic_reporting: boolean;
|
||||
mobile_app_access: boolean;
|
||||
email_support: boolean;
|
||||
easy_step_by_step_onboarding: boolean;
|
||||
|
||||
// Starter+ features
|
||||
basic_forecasting?: boolean;
|
||||
demand_prediction?: boolean;
|
||||
waste_tracking?: boolean;
|
||||
order_management?: boolean;
|
||||
customer_management?: boolean;
|
||||
supplier_management?: boolean;
|
||||
batch_tracking?: boolean;
|
||||
expiry_alerts?: boolean;
|
||||
|
||||
// Professional+ features
|
||||
advanced_analytics?: boolean;
|
||||
custom_reports?: boolean;
|
||||
sales_analytics?: boolean;
|
||||
supplier_performance?: boolean;
|
||||
waste_analysis?: boolean;
|
||||
profitability_analysis?: boolean;
|
||||
weather_data_integration?: boolean;
|
||||
traffic_data_integration?: boolean;
|
||||
multi_location_support?: boolean;
|
||||
location_comparison?: boolean;
|
||||
inventory_transfer?: boolean;
|
||||
batch_scaling?: boolean;
|
||||
recipe_feasibility_check?: boolean;
|
||||
seasonal_patterns?: boolean;
|
||||
longer_forecast_horizon?: boolean;
|
||||
pos_integration?: boolean;
|
||||
accounting_export?: boolean;
|
||||
basic_api_access?: boolean;
|
||||
priority_email_support?: boolean;
|
||||
phone_support?: boolean;
|
||||
|
||||
// Enterprise features
|
||||
scenario_modeling?: boolean;
|
||||
what_if_analysis?: boolean;
|
||||
risk_assessment?: boolean;
|
||||
advanced_ml_parameters?: boolean;
|
||||
model_artifacts_access?: boolean;
|
||||
custom_algorithms?: boolean;
|
||||
full_api_access?: boolean;
|
||||
unlimited_webhooks?: boolean;
|
||||
erp_integration?: boolean;
|
||||
custom_integrations?: boolean;
|
||||
multi_tenant_management?: boolean;
|
||||
white_label_option?: boolean;
|
||||
custom_branding?: boolean;
|
||||
sso_saml?: boolean;
|
||||
advanced_permissions?: boolean;
|
||||
audit_logs_export?: boolean;
|
||||
compliance_reports?: boolean;
|
||||
benchmarking?: boolean;
|
||||
competitive_analysis?: boolean;
|
||||
market_insights?: boolean;
|
||||
predictive_maintenance?: boolean;
|
||||
dedicated_account_manager?: boolean;
|
||||
priority_support?: boolean;
|
||||
support_24_7?: boolean;
|
||||
custom_training?: boolean;
|
||||
onsite_support?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLAN METADATA
|
||||
// ============================================================================
|
||||
|
||||
export interface PlanMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
tagline: string;
|
||||
popular: boolean;
|
||||
monthly_price: number;
|
||||
yearly_price: number;
|
||||
trial_days: number;
|
||||
features: string[]; // List of feature keys
|
||||
limits: {
|
||||
users: number | null;
|
||||
locations: number | null;
|
||||
products: number | null;
|
||||
forecasts_per_day: number | null;
|
||||
};
|
||||
support: string;
|
||||
recommended_for: string;
|
||||
contact_sales?: boolean;
|
||||
}
|
||||
|
||||
export interface AvailablePlans {
|
||||
plans: {
|
||||
[key in SubscriptionTier]: PlanMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USAGE & SUBSCRIPTION STATUS
|
||||
// ============================================================================
|
||||
|
||||
export interface UsageMetric {
|
||||
current: number;
|
||||
limit: number | null;
|
||||
unlimited: boolean;
|
||||
usage_percentage: number;
|
||||
}
|
||||
|
||||
export interface CurrentUsage {
|
||||
// Team & Organization
|
||||
users: UsageMetric;
|
||||
locations: UsageMetric;
|
||||
|
||||
// Product & Inventory
|
||||
products: UsageMetric;
|
||||
recipes: UsageMetric;
|
||||
suppliers: UsageMetric;
|
||||
|
||||
// ML & Analytics (Daily)
|
||||
training_jobs_today: UsageMetric;
|
||||
forecasts_today: UsageMetric;
|
||||
|
||||
// API Usage (Hourly)
|
||||
api_calls_this_hour: UsageMetric;
|
||||
|
||||
// Storage
|
||||
file_storage_used_gb: UsageMetric;
|
||||
}
|
||||
|
||||
export interface UsageSummary {
|
||||
plan: SubscriptionTier;
|
||||
status: 'active' | 'inactive' | 'trialing' | 'past_due' | 'cancelled';
|
||||
billing_cycle: BillingCycle;
|
||||
monthly_price: number;
|
||||
next_billing_date: string;
|
||||
trial_ends_at?: string;
|
||||
usage: CurrentUsage;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FEATURE & QUOTA CHECKS
|
||||
// ============================================================================
|
||||
|
||||
export interface FeatureCheckRequest {
|
||||
feature_name: string;
|
||||
tenant_id: string;
|
||||
@@ -23,80 +218,112 @@ export interface FeatureCheckRequest {
|
||||
|
||||
export interface FeatureCheckResponse {
|
||||
enabled: boolean;
|
||||
limit?: number;
|
||||
current_usage?: number;
|
||||
requires_upgrade: boolean;
|
||||
required_tier?: SubscriptionTier;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UsageCheckRequest {
|
||||
resource_type: 'users' | 'sales_records' | 'inventory_items' | 'api_requests';
|
||||
export interface QuotaCheckRequest {
|
||||
quota_type: string;
|
||||
tenant_id: string;
|
||||
requested_amount?: number;
|
||||
}
|
||||
|
||||
export interface UsageCheckResponse {
|
||||
export interface QuotaCheckResponse {
|
||||
allowed: boolean;
|
||||
limit: number;
|
||||
current_usage: number;
|
||||
remaining: number;
|
||||
current: number;
|
||||
limit: number | null;
|
||||
remaining: number | null;
|
||||
reset_at?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UsageSummary {
|
||||
plan: string;
|
||||
status: 'active' | 'inactive' | 'past_due' | 'cancelled';
|
||||
monthly_price: number;
|
||||
next_billing_date: 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
name: string;
|
||||
description: string;
|
||||
monthly_price: number;
|
||||
max_users: number;
|
||||
max_locations: number;
|
||||
max_products: number;
|
||||
popular?: boolean;
|
||||
contact_sales?: boolean;
|
||||
}
|
||||
|
||||
export interface AvailablePlans {
|
||||
plans: {
|
||||
[key: string]: Plan;
|
||||
};
|
||||
}
|
||||
// ============================================================================
|
||||
// PLAN MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
export interface PlanUpgradeValidation {
|
||||
can_upgrade: boolean;
|
||||
from_tier: SubscriptionTier;
|
||||
to_tier: SubscriptionTier;
|
||||
price_difference: number;
|
||||
prorated_amount?: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface PlanUpgradeRequest {
|
||||
tenant_id: string;
|
||||
new_tier: SubscriptionTier;
|
||||
billing_cycle: BillingCycle;
|
||||
}
|
||||
|
||||
export interface PlanUpgradeResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
new_plan: SubscriptionTier;
|
||||
effective_date: string;
|
||||
}
|
||||
|
||||
// Analytics access levels
|
||||
export interface SubscriptionInvoice {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
status: 'paid' | 'pending' | 'failed';
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
download_url?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
// Plan hierarchy for comparison
|
||||
export const PLAN_HIERARCHY: Record<SubscriptionTier, number> = {
|
||||
[SUBSCRIPTION_TIERS.STARTER]: 1,
|
||||
[SUBSCRIPTION_TIERS.PROFESSIONAL]: 2,
|
||||
[SUBSCRIPTION_TIERS.ENTERPRISE]: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a plan meets minimum tier requirement
|
||||
*/
|
||||
export function doesPlanMeetMinimum(
|
||||
userPlan: SubscriptionTier,
|
||||
requiredPlan: SubscriptionTier
|
||||
): boolean {
|
||||
return PLAN_HIERARCHY[userPlan] >= PLAN_HIERARCHY[requiredPlan];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan display color
|
||||
*/
|
||||
export function getPlanColor(tier: SubscriptionTier): string {
|
||||
switch (tier) {
|
||||
case SUBSCRIPTION_TIERS.STARTER:
|
||||
return 'blue';
|
||||
case SUBSCRIPTION_TIERS.PROFESSIONAL:
|
||||
return 'purple';
|
||||
case SUBSCRIPTION_TIERS.ENTERPRISE:
|
||||
return 'amber';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate discount percentage for yearly billing
|
||||
*/
|
||||
export function getYearlyDiscountPercentage(monthlyPrice: number, yearlyPrice: number): number {
|
||||
const yearlyAnnual = monthlyPrice * 12;
|
||||
const discount = ((yearlyAnnual - yearlyPrice) / yearlyAnnual) * 100;
|
||||
return Math.round(discount);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ANALYTICS LEVELS (for route-based analytics restrictions)
|
||||
// ============================================================================
|
||||
|
||||
export const ANALYTICS_LEVELS = {
|
||||
NONE: 'none',
|
||||
BASIC: 'basic',
|
||||
@@ -106,26 +333,9 @@ export const ANALYTICS_LEVELS = {
|
||||
|
||||
export type AnalyticsLevel = typeof ANALYTICS_LEVELS[keyof typeof ANALYTICS_LEVELS];
|
||||
|
||||
// Plan keys
|
||||
export const SUBSCRIPTION_PLANS = {
|
||||
STARTER: 'starter',
|
||||
PROFESSIONAL: 'professional',
|
||||
ENTERPRISE: 'enterprise'
|
||||
} as const;
|
||||
|
||||
export type SubscriptionPlanKey = typeof SUBSCRIPTION_PLANS[keyof typeof SUBSCRIPTION_PLANS];
|
||||
|
||||
// Plan hierarchy for comparison
|
||||
export const PLAN_HIERARCHY: Record<SubscriptionPlanKey, number> = {
|
||||
[SUBSCRIPTION_PLANS.STARTER]: 1,
|
||||
[SUBSCRIPTION_PLANS.PROFESSIONAL]: 2,
|
||||
[SUBSCRIPTION_PLANS.ENTERPRISE]: 3
|
||||
};
|
||||
|
||||
// Analytics level hierarchy
|
||||
export const ANALYTICS_HIERARCHY: Record<AnalyticsLevel, number> = {
|
||||
[ANALYTICS_LEVELS.NONE]: 0,
|
||||
[ANALYTICS_LEVELS.BASIC]: 1,
|
||||
[ANALYTICS_LEVELS.ADVANCED]: 2,
|
||||
[ANALYTICS_LEVELS.PREDICTIVE]: 3
|
||||
};
|
||||
};
|
||||
|
||||
@@ -168,6 +168,8 @@ export interface TrainingJobStatus {
|
||||
products_completed: number;
|
||||
products_failed: number;
|
||||
error_message?: string | null;
|
||||
estimated_time_remaining_seconds?: number | null; // Estimated time remaining in seconds
|
||||
message?: string | null; // Optional status message
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user