UNPKG

editia-core

Version:

Core services and utilities for Editia applications - Authentication, Monetization, Video Generation Types, and Database Management

449 lines 16.8 kB
"use strict"; /** * Editia Monetization Service - Backend * * This service centralizes all monetization logic for backend operations. * It provides methods to check feature access, validate usage limits, * and manage subscription-based restrictions. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MonetizationService = exports.isMonetizationError = exports.parseMonetizationError = exports.MonetizationError = void 0; const types_1 = require("../../types"); const monetization_1 = require("../../types/monetization"); // ============================================================================ // // Monetization Errors // // ============================================================================ class MonetizationError extends Error { constructor(message, code) { super(message); this.name = 'MonetizationError'; this.code = code; } } exports.MonetizationError = MonetizationError; // Parse the error code from the error message to show in the UI function parseMonetizationError(error) { return error.message; } exports.parseMonetizationError = parseMonetizationError; // Check if the error is a MonetizationError function isMonetizationError(error) { return error instanceof MonetizationError; } exports.isMonetizationError = isMonetizationError; // ============================================================================ // SERVICE IMPLEMENTATION // ============================================================================ class MonetizationService { constructor(config) { this.cache = new Map(); this.cacheTimeout = 5 * 60 * 1000; // 5 minutes this.supabaseClient = config.supabaseClient; this.environment = config.environment || 'development'; } /** * Get singleton instance */ static getInstance(config) { if (!MonetizationService.instance && config) { MonetizationService.instance = new MonetizationService(config); } if (!MonetizationService.instance) { throw new Error('MonetizationService must be initialized with config first'); } return MonetizationService.instance; } /** * Initialize the service (call once at startup) */ static initialize(config) { if (!MonetizationService.instance) { MonetizationService.instance = new MonetizationService(config); } } // ============================================================================ // CORE MONETIZATION CHECKS // ============================================================================ /** * Check if user has access to a specific feature */ async checkFeatureAccess(userId, featureId) { try { // Validate feature ID if (!(0, monetization_1.isValidFeatureId)(featureId)) { return { hasAccess: false, requiredPlan: null, remainingUsage: 0, totalLimit: 0, currentPlan: 'free', error: `Invalid feature ID: ${featureId}`, }; } // Get user usage data const userUsage = await this.getUserUsage(userId); if (!userUsage) { return { hasAccess: false, requiredPlan: null, remainingUsage: 0, totalLimit: 0, currentPlan: 'free', error: 'User usage not found', }; } // Get feature requirements const featureRequirements = await this.getFeatureRequirements(featureId); const requiredPlan = featureRequirements?.required_plan; // Check plan access const hasAccess = !requiredPlan || (0, types_1.hasPlanAccess)(userUsage.current_plan_id, requiredPlan); // Get usage info for the feature const usageInfo = this.getUsageInfoForFeature(featureId, userUsage); return { hasAccess, requiredPlan, remainingUsage: (0, types_1.calculateRemainingUsage)(usageInfo.used, usageInfo.total), totalLimit: usageInfo.total, currentPlan: userUsage.current_plan_id, }; } catch (error) { console.error('Error checking feature access:', error); return { hasAccess: false, requiredPlan: null, remainingUsage: 0, totalLimit: 0, currentPlan: 'free', error: 'Service error', }; } } /** * Validate if user can perform an action (e.g., generate video, upload file) */ async validateUsage(userId, action) { try { // Validate action if (!(0, monetization_1.isValidAction)(action)) { return { isValid: false, remainingUsage: 0, totalLimit: 0, error: `Invalid action: ${action}`, }; } const userUsage = await this.getUserUsage(userId); if (!userUsage) { return { isValid: false, remainingUsage: 0, totalLimit: 0, error: 'User usage not found', }; } const usageInfo = this.getUsageInfoForAction(action, userUsage); const isValid = !(0, types_1.hasReachedLimit)(usageInfo.used, usageInfo.total); return { isValid, remainingUsage: (0, types_1.calculateRemainingUsage)(usageInfo.used, usageInfo.total), totalLimit: usageInfo.total, error: isValid ? undefined : 'Usage limit reached', }; } catch (error) { console.error('Error validating usage:', error); return { isValid: false, remainingUsage: 0, totalLimit: 0, error: 'Service error', }; } } /** * Comprehensive check for feature access and usage validation */ async checkMonetization(userId, featureId) { try { // Validate feature ID if (!(0, monetization_1.isValidFeatureId)(featureId)) { return { success: false, hasAccess: false, currentPlan: 'free', remainingUsage: 0, totalLimit: 0, error: `Invalid feature ID: ${featureId}`, }; } // Check feature access const accessResult = await this.checkFeatureAccess(userId, featureId); if (!accessResult.hasAccess) { return { success: false, hasAccess: false, currentPlan: accessResult.currentPlan, remainingUsage: 0, totalLimit: 0, error: `Feature requires ${accessResult.requiredPlan} plan`, details: { featureId, requiredPlan: accessResult.requiredPlan, usageType: (0, monetization_1.getUsageFieldForFeature)(featureId), }, }; } // Check usage limits const action = (0, monetization_1.getActionForFeature)(featureId); const usageResult = await this.validateUsage(userId, action); if (!usageResult.isValid) { return { success: false, hasAccess: true, currentPlan: accessResult.currentPlan, remainingUsage: usageResult.remainingUsage, totalLimit: usageResult.totalLimit, error: 'Usage limit reached', details: { featureId, requiredPlan: accessResult.requiredPlan, usageType: (0, monetization_1.getUsageFieldForFeature)(featureId), }, }; } return { success: true, hasAccess: true, currentPlan: accessResult.currentPlan, remainingUsage: usageResult.remainingUsage, totalLimit: usageResult.totalLimit, details: { featureId, requiredPlan: accessResult.requiredPlan, usageType: (0, monetization_1.getUsageFieldForFeature)(featureId), }, }; } catch (error) { console.error('Error in monetization check:', error); return { success: false, hasAccess: false, currentPlan: 'free', remainingUsage: 0, totalLimit: 0, error: 'Service error', }; } } // ============================================================================ // USAGE MANAGEMENT // ============================================================================ /** * Increment usage for a specific action */ async incrementUsage(userId, action) { try { // Validate action if (!(0, monetization_1.isValidAction)(action)) { console.error(`Invalid action: ${action}`); return false; } const usageField = (0, monetization_1.getUsageFieldForAction)(action); const { error } = await this.supabaseClient.rpc('increment_user_usage', { p_user_id: userId, p_field_to_increment: usageField }); if (error) { console.error('Error incrementing usage:', error); return false; } // Clear cache for this user this.clearUserCache(userId); return true; } catch (error) { console.error('Error incrementing usage:', error); return false; } } /** * Decrement usage for a specific action */ async decrementUsage(userId, action) { try { // Validate action if (!(0, monetization_1.isValidAction)(action)) { console.error(`Invalid action: ${action}`); return false; } const usageField = (0, monetization_1.getUsageFieldForAction)(action); const { error } = await this.supabaseClient.rpc('decrement_user_usage', { p_user_id: userId, p_field_to_decrement: usageField }); if (error) { console.error('Error decrementing usage:', error); return false; } // Clear cache for this user this.clearUserCache(userId); return true; } catch (error) { console.error('Error decrementing usage:', error); return false; } } /** * Get current usage for a user */ async getUserUsage(userId) { const cacheKey = `usage_${userId}`; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.data; } try { const { data, error } = await this.supabaseClient .from('user_usage') .select('*') .eq('user_id', userId) .single(); if (error) { console.error('Error fetching user usage:', error); return null; } this.cache.set(cacheKey, { data, timestamp: Date.now() }); return data; } catch (error) { console.error('Error fetching user usage:', error); return null; } } // ============================================================================ // FEATURE MANAGEMENT // ============================================================================ /** * Get feature requirements from database */ async getFeatureRequirements(featureId) { const cacheKey = `feature_${featureId}`; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.data; } try { const { data, error } = await this.supabaseClient .from('feature_flags') .select('*') .eq('id', featureId) .eq('is_active', true) .single(); if (error) { console.error('Error fetching feature requirements:', error); return null; } this.cache.set(cacheKey, { data, timestamp: Date.now() }); return data; } catch (error) { console.error('Error fetching feature requirements:', error); return null; } } // ============================================================================ // UTILITY METHODS // ============================================================================ /** * Get usage information for a specific feature */ getUsageInfoForFeature(featureId, userUsage) { const usageField = (0, monetization_1.getUsageFieldForFeature)(featureId); return this.getUsageInfoForField(usageField, userUsage); } /** * Get usage information for a specific action */ getUsageInfoForAction(action, userUsage) { const usageField = (0, monetization_1.getUsageFieldForAction)(action); return this.getUsageInfoForField(usageField, userUsage); } /** * Get usage information for a specific field */ getUsageInfoForField(usageField, userUsage) { const fieldMap = { 'videos_generated': { used: userUsage.videos_generated, total: userUsage.videos_generated_limit, }, 'source_videos_used': { used: userUsage.source_videos_used, total: userUsage.source_videos_limit, }, 'voice_clones_used': { used: userUsage.voice_clones_used, total: userUsage.voice_clones_limit, }, 'account_analysis_used': { used: userUsage.account_analysis_used, total: userUsage.account_analysis_limit, }, 'script_conversations_used': { used: userUsage.script_conversations_used, total: userUsage.script_conversations_limit, }, }; const info = fieldMap[usageField] || { used: 0, total: 0 }; const remaining = (0, types_1.calculateRemainingUsage)(info.used, info.total); const percentage = info.total > 0 ? (info.used / info.total) * 100 : 0; return { used: info.used, total: info.total, remaining, percentage, }; } /** * Clear cache for a specific user */ clearUserCache(userId) { const keysToDelete = Array.from(this.cache.keys()).filter(key => key.includes(userId)); keysToDelete.forEach(key => this.cache.delete(key)); } /** * Clear all cache */ clearCache() { this.cache.clear(); } // ============================================================================ // DEVELOPMENT HELPERS // ============================================================================ /** * Get debug information for a user (development only) */ async getDebugInfo(userId) { if (this.environment !== 'development') { throw new Error('Debug info only available in development'); } const userUsage = await this.getUserUsage(userId); const featureChecks = await Promise.all(Object.values(types_1.FEATURE_FLAGS).map(async (featureId) => ({ featureId, access: await this.checkFeatureAccess(userId, featureId), }))); return { userUsage, featureChecks, environment: this.environment, cacheSize: this.cache.size, }; } } exports.MonetizationService = MonetizationService; //# sourceMappingURL=monetization-service.js.map