/** * Cost control and quota management service * Used to manage API call costs for Vision Pipeline */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../user/user.entity'; export interface UserQuota { userId: string; monthlyCost: number; // Current month used cost maxCost: number; // Monthly max cost remaining: number; // Remaining cost lastReset: Date; // Last reset time } export interface CostEstimate { estimatedCost: number; // Estimated cost estimatedTime: number; // Estimated time(seconds) pageBreakdown: { // Per-page breakdown pageIndex: number; cost: number; }[]; } @Injectable() export class CostControlService { private readonly logger = new Logger(CostControlService.name); private readonly COST_PER_PAGE = 0.01; // Cost per page(USD) private readonly DEFAULT_MONTHLY_LIMIT = 100; // Default monthly limit(USD) constructor( private configService: ConfigService, @InjectRepository(User) private userRepository: Repository, ) { } /** * Estimate processing cost */ estimateCost(pageCount: number, quality: 'low' | 'medium' | 'high' = 'medium'): CostEstimate { // Adjust cost coefficient based on quality const qualityMultiplier = { low: 0.5, medium: 1.0, high: 1.5, }; const baseCost = pageCount * this.COST_PER_PAGE * qualityMultiplier[quality]; const estimatedTime = pageCount * 3; // // Approximately 3 seconds const pageBreakdown = Array.from({ length: pageCount }, (_, i) => ({ pageIndex: i + 1, cost: this.COST_PER_PAGE * qualityMultiplier[quality], })); return { estimatedCost: baseCost, estimatedTime, pageBreakdown, }; } /** * Check user quota */ async checkQuota(userId: string, estimatedCost: number): Promise<{ allowed: boolean; quota: UserQuota; reason?: string; }> { const quota = await this.getUserQuota(userId); // Check monthly reset this.checkAndResetMonthlyQuota(quota); if (quota.remaining < estimatedCost) { this.logger.warn( `User ${userId} quota insufficient: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`, ); return { allowed: false, quota, reason: `Insufficient quota: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`, }; } return { allowed: true, quota, }; } /** * Deduct from quota */ async deductQuota(userId: string, actualCost: number): Promise { const quota = await this.getUserQuota(userId); quota.monthlyCost += actualCost; quota.remaining = quota.maxCost - quota.monthlyCost; await this.userRepository.update(userId, { monthlyCost: quota.monthlyCost, }); this.logger.log( `Deducted $${actualCost.toFixed(2)} from user ${userId} quota. Remaining: $${quota.remaining.toFixed(2)}`, ); } /** * Get user quota */ async getUserQuota(userId: string): Promise { const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) { throw new Error(`User ${userId} does not exist`); } // Use default if user has no quota info const monthlyCost = user.monthlyCost || 0; const maxCost = user.maxCost || this.DEFAULT_MONTHLY_LIMIT; const lastReset = user.lastQuotaReset || new Date(); return { userId, monthlyCost, maxCost, remaining: maxCost - monthlyCost, lastReset, }; } /** * Check and reset monthly quota */ private checkAndResetMonthlyQuota(quota: UserQuota): void { const now = new Date(); const lastReset = quota.lastReset; // Check if crossed month if ( now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear() ) { this.logger.log(`Reset monthly quota for user ${quota.userId}`); // Reset quota quota.monthlyCost = 0; quota.remaining = quota.maxCost; quota.lastReset = now; // Update database this.userRepository.update(quota.userId, { monthlyCost: 0, lastQuotaReset: now, }); } } /** * Set user quota limit */ async setQuotaLimit(userId: string, maxCost: number): Promise { await this.userRepository.update(userId, { maxCost }); this.logger.log(`Set quota limit to $${maxCost} for user ${userId}`); } /** * Get cost report */ async getCostReport(userId: string, days: number = 30): Promise<{ totalCost: number; dailyAverage: number; pageStats: { totalPages: number; avgCostPerPage: number; }; quotaUsage: number; // Percentage }> { const quota = await this.getUserQuota(userId); const usagePercent = (quota.monthlyCost / quota.maxCost) * 100; // Query history records here(if implemented) // Return current quota info temporarily return { totalCost: quota.monthlyCost, dailyAverage: quota.monthlyCost / Math.max(days, 1), pageStats: { totalPages: Math.floor(quota.monthlyCost / this.COST_PER_PAGE), avgCostPerPage: this.COST_PER_PAGE, }, quotaUsage: usagePercent, }; } /** * Check cost warning threshold */ async checkWarningThreshold(userId: string): Promise<{ shouldWarn: boolean; message: string; }> { const quota = await this.getUserQuota(userId); const usagePercent = (quota.monthlyCost / quota.maxCost) * 100; if (usagePercent >= 90) { return { shouldWarn: true, message: `⚠️ Quota usage reached ${usagePercent.toFixed(1)}%. Remaining: $${quota.remaining.toFixed(2)}`, }; } if (usagePercent >= 75) { return { shouldWarn: true, message: `💡 Quota usage at ${usagePercent.toFixed(1)}%. Please monitor your costs carefully`, }; } return { shouldWarn: false, message: '', }; } /** * Format cost display */ formatCost(cost: number): string { return `$${cost.toFixed(2)}`; } /** * Format time display */ formatTime(seconds: number): string { if (seconds < 60) { return `${seconds.toFixed(0)}s`; } const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}m ${remainingSeconds.toFixed(0)}s`; } }