diff --git a/backend/backend-dev-spec.json b/backend/backend-dev-spec.json index 6db9c816d..30a4ba06d 100644 --- a/backend/backend-dev-spec.json +++ b/backend/backend-dev-spec.json @@ -4202,10 +4202,6 @@ "type": "boolean", "description": "Indicates if the user group is builtin and cannot be deleted." }, - "monthlyTokens": { - "type": "number", - "description": "The monthly allowed tokens for all users in the group." - }, "monthlyUserTokens": { "type": "number", "description": "The monthly allowed tokens per user in the group." @@ -4233,10 +4229,6 @@ "type": "string", "description": "The display name." }, - "monthlyTokens": { - "type": "number", - "description": "The monthly allowed tokens for all users in the group." - }, "monthlyUserTokens": { "type": "number", "description": "The monthly allowed tokens per user in the group." diff --git a/backend/package-lock.json b/backend/package-lock.json index 725bb73b5..959fcc384 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -488,6 +488,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics-cli/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -502,6 +520,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -565,6 +584,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -579,6 +616,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -889,7 +927,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1606,7 +1643,6 @@ "node_modules/@grpc/grpc-js": { "version": "1.13.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -1618,7 +1654,6 @@ "node_modules/@grpc/proto-loader": { "version": "0.7.15", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -2844,7 +2879,6 @@ "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", @@ -2957,7 +2991,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -3058,7 +3091,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.9.tgz", "integrity": "sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.1.0", "iterare": "1.2.1", @@ -3114,7 +3146,6 @@ "integrity": "sha512-a00B0BM4X+9z+t3UxJqIZlemIwCQdYoPKrMcM+ky4z3pkqqG1eTWexjs+YXpGObnLnjtMPVKWlcZHp3adDYvUw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3201,7 +3232,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.8.tgz", "integrity": "sha512-rL6pZH9BW7BnL5X2eWbJMtt86uloAKjFgyY5+L2UkizgfEp7rgAs0+Z1z0BcW2Pgu5+q8O7RKPNyHJ/9ZNz/ZQ==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -3309,6 +3339,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@nestjs/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3323,6 +3371,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -3483,7 +3532,6 @@ "node_modules/@nestjs/typeorm": { "version": "11.0.0", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -3668,7 +3716,6 @@ "node_modules/@opentelemetry/api": { "version": "1.9.0", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3765,7 +3812,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -5664,7 +5710,6 @@ "version": "9.6.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5698,7 +5743,6 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -5821,7 +5865,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -6084,7 +6127,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6792,7 +6834,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6854,7 +6895,6 @@ "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.87.tgz", "integrity": "sha512-9Cjx7o8IY9zAczigX0Tk/BaQwjPe/M6DpEjejKSBNrf8mOPIvyM+pJLqJSC10IsKci3FPsnaizJeJhoetU1Wfw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@ai-sdk/gateway": "2.0.6", "@ai-sdk/provider": "2.0.0", @@ -6871,7 +6911,6 @@ "node_modules/ajv": { "version": "6.12.6", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7375,7 +7414,6 @@ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7829,7 +7867,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -8160,13 +8197,11 @@ }, "node_modules/class-transformer": { "version": "0.5.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -9441,7 +9476,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9502,7 +9536,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9907,7 +9940,6 @@ "node_modules/express": { "version": "5.1.0", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -11681,7 +11713,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -13880,13 +13911,13 @@ } }, "node_modules/pause": { - "version": "0.0.1" + "version": "0.0.1", + "peer": true }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -14132,7 +14163,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14185,7 +14215,6 @@ "node_modules/prom-client": { "version": "15.1.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" @@ -14495,8 +14524,7 @@ }, "node_modules/reflect-metadata": { "version": "0.2.2", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -14735,7 +14763,6 @@ "node_modules/rxjs": { "version": "7.8.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -15647,7 +15674,6 @@ "version": "8.17.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16008,7 +16034,6 @@ "version": "10.9.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -16215,7 +16240,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -16413,7 +16437,6 @@ "version": "5.8.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16673,7 +16696,6 @@ "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16739,7 +16761,6 @@ "version": "8.17.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16959,7 +16980,6 @@ "node_modules/winston": { "version": "3.17.0", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -17288,7 +17308,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz", "integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/src/controllers/usages/usages.e2e.spec.ts b/backend/src/controllers/usages/usages.e2e.spec.ts index c8d752a8a..5973ff8c4 100644 --- a/backend/src/controllers/usages/usages.e2e.spec.ts +++ b/backend/src/controllers/usages/usages.e2e.spec.ts @@ -244,7 +244,6 @@ async function createUsageEntity(date: Date, userEntity: UserEntity, usageReposi const usageEntity = new UsageEntity(); usageEntity.date = startOfDay(date); usageEntity.userId = userEntity.id; - usageEntity.userGroup = userEntity.userGroups?.[0]?.id ?? 'admin'; usageEntity.counter = 'token_usage'; usageEntity.key = 'azure-open-ai'; usageEntity.subKey = 'gpt-4o'; diff --git a/backend/src/controllers/users/dtos/index.ts b/backend/src/controllers/users/dtos/index.ts index c6229fa89..45876401d 100644 --- a/backend/src/controllers/users/dtos/index.ts +++ b/backend/src/controllers/users/dtos/index.ts @@ -165,12 +165,6 @@ export class UpsertUserGroupDto { @IsString() name!: string; - @ApiProperty({ - description: 'The monthly allowed tokens for all users in the group.', - required: false, - }) - monthlyTokens?: number; - @ApiProperty({ description: 'The monthly allowed tokens per user in the group.', required: false, @@ -203,12 +197,6 @@ export class UserGroupDto { }) isBuiltIn!: boolean; - @ApiProperty({ - description: 'The monthly allowed tokens for all users in the group.', - required: false, - }) - monthlyTokens?: number; - @ApiProperty({ description: 'The monthly allowed tokens per user in the group.', required: false, @@ -220,7 +208,6 @@ export class UserGroupDto { result.id = source.id; result.isAdmin = source.isAdmin; result.isBuiltIn = source.isBuiltIn; - result.monthlyTokens = source.monthlyTokens; result.monthlyUserTokens = source.monthlyUserTokens; result.name = source.name; diff --git a/backend/src/controllers/users/user-groups.e2e.spec.ts b/backend/src/controllers/users/user-groups.e2e.spec.ts index 1603c4ab2..425f54ba6 100644 --- a/backend/src/controllers/users/user-groups.e2e.spec.ts +++ b/backend/src/controllers/users/user-groups.e2e.spec.ts @@ -45,7 +45,6 @@ describe('User Group', () => { it('should create user group', async () => { const newUserGroup = { name: 'test-group', - monthlyTokens: 1000, monthlyUserTokens: 1000, }; @@ -54,7 +53,6 @@ describe('User Group', () => { const typedBody = response.body as UserGroupDto; expect(typedBody.id).toBeDefined(); expect(typedBody.name).toBe(newUserGroup.name); - expect(typedBody.monthlyTokens).toBe(newUserGroup.monthlyTokens); expect(typedBody.monthlyUserTokens).toBe(newUserGroup.monthlyUserTokens); expect(typedBody.isAdmin).toBe(false); expect(typedBody.isBuiltIn).toBe(false); @@ -65,7 +63,6 @@ describe('User Group', () => { const updatedUserGroup = { name: 'test-group', - monthlyTokens: 2000, monthlyUserTokens: 2000, }; @@ -77,7 +74,6 @@ describe('User Group', () => { const typedBody = response.body as UserGroupDto; expect(typedBody.id).toBe(userGroupToUpdate?.id); expect(typedBody.name).toBe(updatedUserGroup.name); - expect(typedBody.monthlyTokens).toBe(updatedUserGroup.monthlyTokens); expect(typedBody.monthlyUserTokens).toBe(updatedUserGroup.monthlyUserTokens); expect(typedBody.isAdmin).toBe(false); expect(typedBody.isBuiltIn).toBe(false); diff --git a/backend/src/domain/chat/middlewares/check-usage-middleware.ts b/backend/src/domain/chat/middlewares/check-usage-middleware.ts index 0b43a8a19..772b9424a 100644 --- a/backend/src/domain/chat/middlewares/check-usage-middleware.ts +++ b/backend/src/domain/chat/middlewares/check-usage-middleware.ts @@ -40,11 +40,11 @@ export class CheckUsageMiddleware implements ChatMiddleware { await next(context); return; } + const userGroup = await this.userGroups.findOneBy({ id: userGroupId }); - const monthlyTokens = userGroup?.monthlyTokens ?? 0; const monthlyUserTokens = userGroup?.monthlyUserTokens ?? 0; - if (!userGroup || (monthlyTokens < 0 && monthlyUserTokens < 0)) { + if (!userGroup || monthlyUserTokens < 0) { await next(context); return; } @@ -52,26 +52,14 @@ export class CheckUsageMiddleware implements ChatMiddleware { const dateFrom = startOfMonth(new Date()); const dateTo = addMonths(dateFrom, 1); - if (monthlyTokens > 0) { - const groupUsage = - (await this.usages.sum('count', { - date: And(MoreThanOrEqual(dateFrom), LessThan(dateTo)), - userGroup: userGroupId, - })) ?? 0; - - if (groupUsage >= monthlyTokens) { - throw new HttpException('Monthly token limit exceeded for user group.', HttpStatus.TOO_MANY_REQUESTS); - } - } - if (monthlyUserTokens > 0) { - const groupUsage = + const userUsage = (await this.usages.sum('count', { date: And(MoreThanOrEqual(dateFrom), LessThan(dateTo)), userId: user.id, })) ?? 0; - if (groupUsage >= monthlyUserTokens) { + if (userUsage >= monthlyUserTokens) { throw new HttpException('Monthly token limit exceeded for user.', HttpStatus.TOO_MANY_REQUESTS); } } diff --git a/backend/src/domain/chat/middlewares/store-usage-middleware.ts b/backend/src/domain/chat/middlewares/store-usage-middleware.ts index 2cc7155cb..e7bc67206 100644 --- a/backend/src/domain/chat/middlewares/store-usage-middleware.ts +++ b/backend/src/domain/chat/middlewares/store-usage-middleware.ts @@ -28,7 +28,6 @@ export class StorageUsageMiddleware implements ChatMiddleware { date: new Date(), key: usage.llm, subKey: usage.model, - userGroup: context.user.userGroupIds?.[0], userId: context.user.id, }); } catch (err) { diff --git a/backend/src/domain/chat/use-cases/rate-message.ts b/backend/src/domain/chat/use-cases/rate-message.ts index 7e6e2877c..ea1546677 100644 --- a/backend/src/domain/chat/use-cases/rate-message.ts +++ b/backend/src/domain/chat/use-cases/rate-message.ts @@ -56,7 +56,6 @@ export class RateMessageHandler implements ICommandHandler; +type Values = Pick; export class CreateUserGroup { constructor(public readonly values: Values) {} @@ -25,12 +25,12 @@ export class CreateUserGroupHandler implements ICommandHandler { const { values } = request; - const { monthlyTokens, monthlyUserTokens, name } = values; + const { monthlyUserTokens, name } = values; const entity = this.userGroups.create({ id: uuid.v4() }); // Assign the object manually to avoid updating unexpected values. - assignDefined(entity, { monthlyTokens, monthlyUserTokens, name }); + assignDefined(entity, { monthlyUserTokens, name }); // Use the save method otherwise we would not get previous values. const created = await this.userGroups.save(entity); diff --git a/backend/src/domain/users/use-cases/update-user-group.ts b/backend/src/domain/users/use-cases/update-user-group.ts index fdeb79a24..ab1668717 100644 --- a/backend/src/domain/users/use-cases/update-user-group.ts +++ b/backend/src/domain/users/use-cases/update-user-group.ts @@ -6,7 +6,7 @@ import { assignWithUndefined } from 'src/lib'; import { UserGroup } from '../interfaces'; import { buildUserGroup } from './utils'; -type Values = Pick; +type Values = Pick; export class UpdateUserGroup { constructor( @@ -28,7 +28,7 @@ export class UpdateUserGroupHandler implements ICommandHandler { const { id, values } = request; - const { monthlyTokens, monthlyUserTokens, name } = values; + const { monthlyUserTokens, name } = values; const entity = await this.userGroups.findOneBy({ id }); @@ -40,7 +40,7 @@ export class UpdateUserGroupHandler implements ICommandHandler { + // Step 1: Create a temporary table with merged usage data + // This merges rows that would become duplicates after removing userGroup + await queryRunner.query(` + CREATE TEMP TABLE usages_merged AS + SELECT + date, + "userId", + counter, + key, + "subKey", + SUM(count) as count + FROM company_chat.usages + GROUP BY date, "userId", counter, key, "subKey" + `); + + // Step 2: Drop the old primary key constraint + await queryRunner.query(`ALTER TABLE company_chat.usages DROP CONSTRAINT "PK_0acc90e335c519dc4e2140320f1"`); + + // Step 3: Truncate the table and reload with merged data + await queryRunner.query(`TRUNCATE company_chat.usages`); + + await queryRunner.query(` + INSERT INTO company_chat.usages (date, "userId", "userGroup", counter, key, "subKey", count) + SELECT date, "userId", '', counter, key, "subKey", count + FROM usages_merged + `); + + // Step 4: Drop the userGroup column + await queryRunner.query(`ALTER TABLE company_chat.usages DROP COLUMN "userGroup"`); + + // Step 5: Add new primary key constraint without userGroup + await queryRunner.query( + `ALTER TABLE company_chat.usages ADD CONSTRAINT "PK_usages_without_group" PRIMARY KEY (date, "userId", counter, key, "subKey")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the new primary key + await queryRunner.query(`ALTER TABLE company_chat.usages DROP CONSTRAINT "PK_usages_without_group"`); + + // Re-add the userGroup column + await queryRunner.query(`ALTER TABLE company_chat.usages ADD COLUMN "userGroup" character varying NOT NULL DEFAULT ''`); + + // Restore the original primary key + await queryRunner.query( + `ALTER TABLE company_chat.usages ADD CONSTRAINT "PK_0acc90e335c519dc4e2140320f1" PRIMARY KEY (date, "userId", "userGroup", counter, key, "subKey")`, + ); + } +} diff --git a/backend/src/migrations/1767646571152-removeMonthlyTokensFromUserGroups.ts b/backend/src/migrations/1767646571152-removeMonthlyTokensFromUserGroups.ts new file mode 100644 index 000000000..28bf5b997 --- /dev/null +++ b/backend/src/migrations/1767646571152-removeMonthlyTokensFromUserGroups.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveMonthlyTokensFromUserGroups1767646571152 implements MigrationInterface { + name = 'RemoveMonthlyTokensFromUserGroups1767646571152'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE company_chat."user-groups" DROP COLUMN "monthlyTokens"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE company_chat."user-groups" ADD COLUMN "monthlyTokens" integer`); + } +} diff --git a/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.tsx b/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.tsx index 4d3c49c60..1b1419cec 100644 --- a/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.tsx +++ b/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.tsx @@ -9,11 +9,6 @@ import { texts } from 'src/texts'; const SCHEME = Yup.object({ name: Yup.string().label(texts.common.name).required(), - monthlyTokens: Yup.number() - .positive() - .label(texts.common.monthlyTokens) - .nullable() - .transform((value: number, originalValue: string) => (originalValue === '' ? null : value)), monthlyUserTokens: Yup.number() .positive() .label(texts.common.monthlyUserTokens) @@ -44,7 +39,7 @@ export function CreateUserGroupDialog({ onClose, onCreate }: CreateUserGroupDial const form = useForm({ resolver: RESOLVER, - defaultValues: { name: '', monthlyUserTokens: null, monthlyTokens: null }, + defaultValues: { name: '', monthlyUserTokens: null }, }); return ( @@ -55,7 +50,6 @@ export function CreateUserGroupDialog({ onClose, onCreate }: CreateUserGroupDial onSubmit={form.handleSubmit((v) => updating.mutate({ name: v.name, - monthlyTokens: v.monthlyTokens ? v.monthlyTokens : undefined, monthlyUserTokens: v.monthlyUserTokens ? v.monthlyUserTokens : undefined, }), )} @@ -80,8 +74,6 @@ export function CreateUserGroupDialog({ onClose, onCreate }: CreateUserGroupDial - - diff --git a/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.ui-unit.spec.tsx b/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.ui-unit.spec.tsx index b5e76600b..3152df8f8 100644 --- a/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.ui-unit.spec.tsx +++ b/frontend/src/pages/admin/user-groups/CreateUserGroupDialog.ui-unit.spec.tsx @@ -22,21 +22,6 @@ describe('User Page', () => { expect(screen.getAllByRole('alert')).toHaveLength(1); }); - it('should alert when monthly tokens goes below zero', async () => { - render( {}} onClose={() => {}} />); - - const user = userEvent.setup(); - const nameInput = screen.getByLabelText(required(texts.common.groupName)); - await user.click(nameInput); - await user.type(nameInput, 'st1'); - const monthlyTokenInput = screen.getByLabelText('Monthly tokens'); - await user.click(monthlyTokenInput); - await user.type(monthlyTokenInput, '-1'); - const saveBtn = screen.getByRole('button', { name: 'Save' }); - await user.click(saveBtn); - expect(screen.getAllByRole('alert')).toHaveLength(1); - }); - it('should alert when monthly tokens/user goes below zero', async () => { render( {}} onClose={() => {}} />); @@ -44,9 +29,6 @@ describe('User Page', () => { const nameInput = screen.getByLabelText(required(texts.common.groupName)); await user.click(nameInput); await user.type(nameInput, 'st1'); - const monthlyTokenInput = screen.getByLabelText('Monthly tokens'); - await user.click(monthlyTokenInput); - await user.type(monthlyTokenInput, '1'); const monthlyUserTokenInput = screen.getByLabelText('Monthly tokens / User'); await user.click(monthlyUserTokenInput); await user.type(monthlyUserTokenInput, '-1'); diff --git a/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.tsx b/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.tsx index 091efce33..c2e2e10f1 100644 --- a/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.tsx +++ b/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.tsx @@ -12,11 +12,6 @@ import { texts } from 'src/texts'; const SCHEME = Yup.object({ name: Yup.string().label(texts.common.name).required(), - monthlyTokens: Yup.number() - .positive() - .label(texts.common.monthlyTokens) - .nullable() - .transform((value: number, originalValue: string) => (originalValue === '' ? null : value)), monthlyUserTokens: Yup.number() .positive() .label(texts.common.monthlyUserTokens) @@ -45,7 +40,7 @@ export function UpdateUserGroupDialog({ onClose, onDelete, onUpdate, target }: U const form = useForm({ resolver: RESOLVER, - defaultValues: { name: '', monthlyUserTokens: null, monthlyTokens: null }, + defaultValues: { name: '', monthlyUserTokens: null }, }); useEffect(() => { form.reset(target); @@ -59,7 +54,6 @@ export function UpdateUserGroupDialog({ onClose, onDelete, onUpdate, target }: U onSubmit={form.handleSubmit((v) => userGroupUpdate.mutate({ name: v.name, - monthlyTokens: v.monthlyTokens ? v.monthlyTokens : undefined, monthlyUserTokens: v.monthlyUserTokens ? v.monthlyUserTokens : undefined, }), )} @@ -85,8 +79,6 @@ export function UpdateUserGroupDialog({ onClose, onDelete, onUpdate, target }: U - - diff --git a/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.ui-unit.spec.tsx b/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.ui-unit.spec.tsx index 80ffb1074..b231c7960 100644 --- a/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.ui-unit.spec.tsx +++ b/frontend/src/pages/admin/user-groups/UpdateUserGroupDialog.ui-unit.spec.tsx @@ -12,7 +12,6 @@ describe('UpdateUserGroupDialog', () => { name: 'St1', isAdmin: false, isBuiltIn: false, - monthlyTokens: 0, monthlyUserTokens: 0, }; @@ -27,7 +26,6 @@ describe('UpdateUserGroupDialog', () => { render(); expect(screen.getByLabelText(required(texts.common.groupName))).toHaveValue(mockUserGroup.name); - expect(screen.getByLabelText(texts.common.monthlyTokens)).toHaveValue(mockUserGroup.monthlyTokens); expect(screen.getByLabelText(texts.common.monthlyUserTokens)).toHaveValue(mockUserGroup.monthlyUserTokens); }); @@ -47,7 +45,6 @@ describe('UpdateUserGroupDialog', () => { const user = userEvent.setup(); const nameInput = screen.getByLabelText(required(texts.common.groupName)); await user.clear(nameInput); - await user.type(screen.getByLabelText(texts.common.monthlyTokens), '1200'); await user.type(screen.getByLabelText(texts.common.monthlyUserTokens), '120'); const saveButton = screen.getByRole('button', { name: texts.common.save }); await user.click(saveButton);