1
18import { Inject, Injectable, OnModuleInit } from "@nestjs/common";
19import type { Request } from "express";
20import type Redis from "ioredis";
21import type { PrismaClient } from "@prisma/client";
22import { PrismaService } from "../../common/prisma.service.js";
23import { RedisService } from "../../common/redis.service.js";
24import { AuditService } from "../../common/audit.service.js";
25import { logger } from "../../common/logger.js";
26import { NotificationsService } from "../notifications/notifications.service.js";
27import { createAiBudgetGuard, type AiBudgetGuard } from "./gateway-provider.js";
28
29@Injectable()
30export class AiBudgetGuardService implements OnModuleInit {
31 private _guard: AiBudgetGuard | null = null;
32
33 constructor(
34 @Inject(PrismaService) private readonly prisma: PrismaService,
35 @Inject(RedisService) private readonly redis: RedisService,
36 @Inject(AuditService) private readonly audit: AuditService,
37 @Inject(NotificationsService) private readonly notifications: NotificationsService
38 ) {}
39
40 onModuleInit(): void {
41 this._guard = createAiBudgetGuard({
42 prisma: this.prisma as unknown as PrismaClient,
43 redis: this.redis.getRedis() as Redis,
44 onBudgetExceeded: (workspaceId, used, limit, req) => this.onBudgetExceeded(workspaceId, used, limit, req)
45 });
46 }
47
48
50 get guard(): AiBudgetGuard {
51 if (!this._guard) throw new Error("AiBudgetGuardService not initialized");
52 return this._guard;
53 }
54
55
56 async onBudgetExceeded(workspaceId: string, used: number, limit: number, req?: Request): Promise<void> {
57 const prisma = this.prisma as unknown as PrismaClient;
58 const admins = await prisma.workspaceMember.findMany({
59 where: { workspaceId, role: { in: ["OWNER", "ADMIN"] } },
60 select: { userId: true }
61 });
62
63 const body =
64 `The workspace AI token budget was reached (${used} used of ${limit} monthly). ` +
65 `New AI calls are blocked until the limit is raised (workspace settings → usageLimits.aiMonthlyTokens).`;
66
67 for (const admin of admins) {
68 try {
69 await this.notifications.notify({
70 workspaceId,
71 userId: admin.userId,
72 type: "AUTOMATION",
73 title: "AI budget exceeded",
74 body,
75 link: { type: "workspace", id: workspaceId }
76 });
77 } catch (err) {
78 logger.warn(`ai budget notification failed for user ${admin.userId}: ${(err as Error).message}`);
79 }
80 }
81
82 await this.audit.audit(req, undefined, {
83 action: "ai.budget_exceeded",
84 resourceType: "workspace",
85 resourceId: workspaceId,
86 workspaceId,
87 metadata: { used, limit, adminsNotified: admins.length }
88 });
89 }
90}
91