cost-control.service.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import { Injectable, Logger } from '@nestjs/common';
  2. import { ConfigService } from '@nestjs/config';
  3. import { InjectRepository } from '@nestjs/typeorm';
  4. import { Repository } from 'typeorm';
  5. import { User } from '../user/user.entity';
  6. export interface UserQuota {
  7. userId: string;
  8. monthlyCost: number;
  9. maxCost: number;
  10. remaining: number;
  11. lastReset: Date;
  12. }
  13. export interface CostEstimate {
  14. estimatedCost: number;
  15. estimatedTime: number;
  16. pageBreakdown: {
  17. pageIndex: number;
  18. cost: number;
  19. }[];
  20. }
  21. @Injectable()
  22. export class CostControlService {
  23. private readonly logger = new Logger(CostControlService.name);
  24. private readonly COST_PER_PAGE = 0.01;
  25. private readonly DEFAULT_MONTHLY_LIMIT = 100;
  26. constructor(
  27. private configService: ConfigService,
  28. @InjectRepository(User)
  29. private userRepository: Repository<User>,
  30. ) { }
  31. estimateCost(pageCount: number, quality: 'low' | 'medium' | 'high' = 'medium'): CostEstimate {
  32. const qualityMultiplier = {
  33. low: 0.5,
  34. medium: 1.0,
  35. high: 1.5,
  36. };
  37. const baseCost = pageCount * this.COST_PER_PAGE * qualityMultiplier[quality];
  38. const estimatedTime = pageCount * 3;
  39. const pageBreakdown = Array.from({ length: pageCount }, (_, i) => ({
  40. pageIndex: i + 1,
  41. cost: this.COST_PER_PAGE * qualityMultiplier[quality],
  42. }));
  43. return {
  44. estimatedCost: baseCost,
  45. estimatedTime,
  46. pageBreakdown,
  47. };
  48. }
  49. async checkQuota(userId: string, estimatedCost: number): Promise<{
  50. allowed: boolean;
  51. quota: UserQuota;
  52. reason?: string;
  53. }> {
  54. const quota = await this.getUserQuota(userId);
  55. this.checkAndResetMonthlyQuota(quota);
  56. if (quota.remaining < estimatedCost) {
  57. this.logger.warn(
  58. `Insufficient quota for user ${userId}: Remaining $${quota.remaining.toFixed(2)}, Required $${estimatedCost.toFixed(2)}`,
  59. );
  60. return {
  61. allowed: false,
  62. quota,
  63. reason: `Insufficient quota: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`,
  64. };
  65. }
  66. return {
  67. allowed: true,
  68. quota,
  69. };
  70. }
  71. async deductQuota(userId: string, actualCost: number): Promise<void> {
  72. const quota = await this.getUserQuota(userId);
  73. quota.monthlyCost += actualCost;
  74. quota.remaining = quota.maxCost - quota.monthlyCost;
  75. await this.userRepository.update(userId, {
  76. monthlyCost: quota.monthlyCost,
  77. });
  78. this.logger.log(
  79. `Deducted $${actualCost.toFixed(2)} from user ${userId}'s quota. Remaining $${quota.remaining.toFixed(2)}`,
  80. );
  81. }
  82. async getUserQuota(userId: string): Promise<UserQuota> {
  83. const user = await this.userRepository.findOne({ where: { id: userId } });
  84. if (!user) {
  85. throw new Error(`User ${userId} does not exist`);
  86. }
  87. const monthlyCost = user.monthlyCost || 0;
  88. const maxCost = user.maxCost || this.DEFAULT_MONTHLY_LIMIT;
  89. const lastReset = user.lastQuotaReset || new Date();
  90. return {
  91. userId,
  92. monthlyCost,
  93. maxCost,
  94. remaining: maxCost - monthlyCost,
  95. lastReset,
  96. };
  97. }
  98. private checkAndResetMonthlyQuota(quota: UserQuota): void {
  99. const now = new Date();
  100. const lastReset = quota.lastReset;
  101. if (
  102. now.getMonth() !== lastReset.getMonth() ||
  103. now.getFullYear() !== lastReset.getFullYear()
  104. ) {
  105. this.logger.log(`Reset monthly quota for user ${quota.userId}`);
  106. quota.monthlyCost = 0;
  107. quota.remaining = quota.maxCost;
  108. quota.lastReset = now;
  109. this.userRepository.update(quota.userId, {
  110. monthlyCost: 0,
  111. lastQuotaReset: now,
  112. });
  113. }
  114. }
  115. async setQuotaLimit(userId: string, maxCost: number): Promise<void> {
  116. await this.userRepository.update(userId, { maxCost });
  117. this.logger.log(`Set quota limit for user ${userId} to $${maxCost}`);
  118. }
  119. async getCostReport(userId: string, days: number = 30): Promise<{
  120. totalCost: number;
  121. dailyAverage: number;
  122. pageStats: {
  123. totalPages: number;
  124. avgCostPerPage: number;
  125. };
  126. quotaUsage: number;
  127. }> {
  128. const quota = await this.getUserQuota(userId);
  129. const usagePercent = (quota.monthlyCost / quota.maxCost) * 100;
  130. return {
  131. totalCost: quota.monthlyCost,
  132. dailyAverage: quota.monthlyCost / Math.max(days, 1),
  133. pageStats: {
  134. totalPages: Math.floor(quota.monthlyCost / this.COST_PER_PAGE),
  135. avgCostPerPage: this.COST_PER_PAGE,
  136. },
  137. quotaUsage: usagePercent,
  138. };
  139. }
  140. async checkWarningThreshold(userId: string): Promise<{
  141. shouldWarn: boolean;
  142. message: string;
  143. }> {
  144. const quota = await this.getUserQuota(userId);
  145. const usagePercent = (quota.monthlyCost / quota.maxCost) * 100;
  146. if (usagePercent >= 90) {
  147. return {
  148. shouldWarn: true,
  149. message: `⚠️ Quota usage has reached ${usagePercent.toFixed(1)}%. Remaining $${quota.remaining.toFixed(2)}`,
  150. };
  151. }
  152. if (usagePercent >= 75) {
  153. return {
  154. shouldWarn: true,
  155. message: `💡 Quota usage ${usagePercent.toFixed(1)}%. Be careful with controlling costs`,
  156. };
  157. }
  158. return {
  159. shouldWarn: false,
  160. message: '',
  161. };
  162. }
  163. formatCost(cost: number): string {
  164. return `$${cost.toFixed(2)}`;
  165. }
  166. formatTime(seconds: number): string {
  167. if (seconds < 60) {
  168. return `${seconds.toFixed(0)}s`;
  169. }
  170. const minutes = Math.floor(seconds / 60);
  171. const remainingSeconds = seconds % 60;
  172. return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
  173. }
  174. }