editia-core
Version:
Core services and utilities for Editia applications - Authentication, Monetization, Video Generation Types, and Database Management
449 lines • 16.8 kB
JavaScript
"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