feat: implement billing, usage tracking, and administrative geocoding features in API with infrastructure updates

This commit is contained in:
Hamza-Ayed
2026-07-28 04:43:23 +03:00
parent 2f7c2066f7
commit dc9d546733
133 changed files with 3599 additions and 356 deletions
+17
View File
@@ -0,0 +1,17 @@
import { UsageService } from './usage.service';
export declare class UsageController {
private usageService;
constructor(usageService: UsageService);
getSummary(req: any): Promise<{
limit: any;
percentage: string;
plan: any;
monthlyUsage: number;
monthlyLimit: any;
totalToday: number;
avgLatency: number;
successRate: number;
lastUpdated: Date;
}>;
getHistory(req: any, days?: number): Promise<any>;
}
+72
View File
@@ -0,0 +1,72 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageController = void 0;
const common_1 = require("@nestjs/common");
const usage_service_1 = require("./usage.service");
const firebase_auth_guard_1 = require("../auth/guards/firebase-auth.guard");
const swagger_1 = require("@nestjs/swagger");
let UsageController = class UsageController {
usageService;
constructor(usageService) {
this.usageService = usageService;
}
async getSummary(req) {
const tenant = req.tenant;
const summary = await this.usageService.getUsageSummary(tenant.id);
const limits = {
FREE: 5000,
STARTER: 25000,
PRO: 100000,
ENTERPRISE: 500000,
};
const limit = limits[tenant.plan] || 5000;
return {
...summary,
limit,
percentage: Math.min(((summary.monthlyUsage / limit) * 100), 100).toFixed(1),
plan: tenant.plan,
};
}
async getHistory(req, days = 30) {
const tenantId = req.tenant.id;
return this.usageService.getUsageHistory(tenantId, days);
}
};
exports.UsageController = UsageController;
__decorate([
(0, common_1.Get)('summary'),
(0, swagger_1.ApiOperation)({ summary: 'Get current month usage summary' }),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], UsageController.prototype, "getSummary", null);
__decorate([
(0, common_1.Get)('history'),
(0, swagger_1.ApiOperation)({ summary: 'Get daily request volume history' }),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Query)('days')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Number]),
__metadata("design:returntype", Promise)
], UsageController.prototype, "getHistory", null);
exports.UsageController = UsageController = __decorate([
(0, swagger_1.ApiTags)('usage'),
(0, swagger_1.ApiBearerAuth)(),
(0, common_1.UseGuards)(firebase_auth_guard_1.FirebaseAuthGuard),
(0, common_1.Controller)('usage'),
__metadata("design:paramtypes", [usage_service_1.UsageService])
], UsageController);
//# sourceMappingURL=usage.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"usage.controller.js","sourceRoot":"","sources":["../../src/usage/usage.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAwE;AACxE,mDAA+C;AAC/C,4EAAuE;AACvE,6CAAuE;AAMhE,IAAM,eAAe,GAArB,MAAM,eAAe;IACN;IAApB,YAAoB,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAI5C,AAAN,KAAK,CAAC,UAAU,CAAQ,GAAQ;QAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAGnE,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,KAAK;YACd,GAAG,EAAE,MAAM;YACX,UAAU,EAAE,MAAM;SACnB,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QAE1C,OAAO;YACL,GAAG,OAAO;YACV,KAAK;YACL,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5E,IAAI,EAAE,MAAM,CAAC,IAAI;SAClB,CAAC;IACJ,CAAC;IAIK,AAAN,KAAK,CAAC,UAAU,CAAQ,GAAQ,EAAiB,OAAe,EAAE;QAChE,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;CACF,CAAA;AAjCY,0CAAe;AAKpB;IAFL,IAAA,YAAG,EAAC,SAAS,CAAC;IACd,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC;IAC3C,WAAA,IAAA,YAAG,GAAE,CAAA;;;;iDAoBtB;AAIK;IAFL,IAAA,YAAG,EAAC,SAAS,CAAC;IACd,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC;IAC5C,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;;;;iDAG/C;0BAhCU,eAAe;IAJ3B,IAAA,iBAAO,EAAC,OAAO,CAAC;IAChB,IAAA,uBAAa,GAAE;IACf,IAAA,kBAAS,EAAC,uCAAiB,CAAC;IAC5B,IAAA,mBAAU,EAAC,OAAO,CAAC;qCAEgB,4BAAY;GADnC,eAAe,CAiC3B"}
+12
View File
@@ -0,0 +1,12 @@
export declare class UsageLog {
id: string;
tenantId: string;
apiKeyId: string;
endpoint: string;
method: string;
statusCode: number;
responseTimeMs: number;
userAgent: string;
ipAddress: string;
createdAt: Date;
}
+73
View File
@@ -0,0 +1,73 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageLog = void 0;
const typeorm_1 = require("typeorm");
let UsageLog = class UsageLog {
id;
tenantId;
apiKeyId;
endpoint;
method;
statusCode;
responseTimeMs;
userAgent;
ipAddress;
createdAt;
};
exports.UsageLog = UsageLog;
__decorate([
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
__metadata("design:type", String)
], UsageLog.prototype, "id", void 0);
__decorate([
(0, typeorm_1.Index)(),
(0, typeorm_1.Column)(),
__metadata("design:type", String)
], UsageLog.prototype, "tenantId", void 0);
__decorate([
(0, typeorm_1.Index)(),
(0, typeorm_1.Column)(),
__metadata("design:type", String)
], UsageLog.prototype, "apiKeyId", void 0);
__decorate([
(0, typeorm_1.Column)(),
__metadata("design:type", String)
], UsageLog.prototype, "endpoint", void 0);
__decorate([
(0, typeorm_1.Column)(),
__metadata("design:type", String)
], UsageLog.prototype, "method", void 0);
__decorate([
(0, typeorm_1.Column)({ type: 'int', default: 200 }),
__metadata("design:type", Number)
], UsageLog.prototype, "statusCode", void 0);
__decorate([
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
__metadata("design:type", Number)
], UsageLog.prototype, "responseTimeMs", void 0);
__decorate([
(0, typeorm_1.Column)({ nullable: true }),
__metadata("design:type", String)
], UsageLog.prototype, "userAgent", void 0);
__decorate([
(0, typeorm_1.Column)({ nullable: true }),
__metadata("design:type", String)
], UsageLog.prototype, "ipAddress", void 0);
__decorate([
(0, typeorm_1.Index)(),
(0, typeorm_1.CreateDateColumn)(),
__metadata("design:type", Date)
], UsageLog.prototype, "createdAt", void 0);
exports.UsageLog = UsageLog = __decorate([
(0, typeorm_1.Entity)('usage_logs')
], UsageLog);
//# sourceMappingURL=usage.entity.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"usage.entity.js","sourceRoot":"","sources":["../../src/usage/usage.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAMiB;AAGV,IAAM,QAAQ,GAAd,MAAM,QAAQ;IAEnB,EAAE,CAAS;IAIX,QAAQ,CAAS;IAIjB,QAAQ,CAAS;IAGjB,QAAQ,CAAS;IAGjB,MAAM,CAAS;IAGf,UAAU,CAAS;IAGnB,cAAc,CAAS;IAGvB,SAAS,CAAS;IAGlB,SAAS,CAAS;IAIlB,SAAS,CAAO;CACjB,CAAA;AAjCY,4BAAQ;AAEnB;IADC,IAAA,gCAAsB,EAAC,MAAM,CAAC;;oCACpB;AAIX;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,GAAE;;0CACQ;AAIjB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,GAAE;;0CACQ;AAGjB;IADC,IAAA,gBAAM,GAAE;;0CACQ;AAGjB;IADC,IAAA,gBAAM,GAAE;;wCACM;AAGf;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;;4CACnB;AAGnB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;gDACb;AAGvB;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2CACT;AAGlB;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2CACT;AAIlB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,0BAAgB,GAAE;8BACR,IAAI;2CAAC;mBAhCL,QAAQ;IADpB,IAAA,gBAAM,EAAC,YAAY,CAAC;GACR,QAAQ,CAiCpB"}
+9
View File
@@ -0,0 +1,9 @@
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { UsageService } from './usage.service';
export declare class UsageInterceptor implements NestInterceptor {
private usageService;
private readonly logger;
constructor(usageService: UsageService);
intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>>;
}
+81
View File
@@ -0,0 +1,81 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var UsageInterceptor_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageInterceptor = void 0;
const common_1 = require("@nestjs/common");
const rxjs_1 = require("rxjs");
const usage_service_1 = require("./usage.service");
const tenant_entity_1 = require("../auth/entities/tenant.entity");
let UsageInterceptor = UsageInterceptor_1 = class UsageInterceptor {
usageService;
logger = new common_1.Logger(UsageInterceptor_1.name);
constructor(usageService) {
this.usageService = usageService;
}
async intercept(context, next) {
const request = context.switchToHttp().getRequest();
const tenant = request.tenant;
const apiKey = request.apiKey;
if (tenant && apiKey && !request.url.includes('/auth/management')) {
this.logger.debug(`📊 Monitoring request for Tenant: ${tenant.id}`);
const plan = tenant.plan || tenant_entity_1.TenantPlan.FREE;
const limit = tenant_entity_1.QUOTA_LIMITS[plan];
const { allowed: quotaAllowed, used: monthlyUsed } = await this.usageService.checkQuota(tenant.id, limit);
if (!quotaAllowed) {
throw new common_1.HttpException({
statusCode: common_1.HttpStatus.TOO_MANY_REQUESTS,
error: 'Quota Exceeded',
message: 'Monthly API usage quota exceeded',
used: monthlyUsed,
limit,
upgrade_url: 'https://map-dashbord.intaleqapp.com/#billing'
}, common_1.HttpStatus.TOO_MANY_REQUESTS);
}
const rpmLimit = tenant_entity_1.RATE_LIMITS[plan];
const { allowed: rateAllowed, used: currentRpm } = await this.usageService.checkRateLimit(tenant.id, rpmLimit);
if (!rateAllowed) {
throw new common_1.HttpException({
statusCode: common_1.HttpStatus.TOO_MANY_REQUESTS,
error: 'Rate Limit Exceeded',
message: `Request rate limit exceeded (${rpmLimit} req/min for ${plan} plan)`,
current_rate: currentRpm,
limit: rpmLimit,
retry_after: '60s'
}, common_1.HttpStatus.TOO_MANY_REQUESTS);
}
}
const startTime = Date.now();
return next.handle().pipe((0, rxjs_1.tap)(() => {
if (tenant && apiKey) {
const response = context.switchToHttp().getResponse();
const responseTimeMs = Date.now() - startTime;
this.logger.log(`📈 Usage Recorded: ${request.method} ${request.url.split('?')[0]} for Tenant ${tenant.id}`);
this.usageService.recordRequest({
tenantId: tenant.id,
apiKeyId: apiKey.id,
endpoint: request.url.split('?')[0],
method: request.method,
statusCode: response.statusCode,
responseTimeMs,
userAgent: request.headers['user-agent'],
ipAddress: request.ip || request.headers['x-forwarded-for'],
});
}
}));
}
};
exports.UsageInterceptor = UsageInterceptor;
exports.UsageInterceptor = UsageInterceptor = UsageInterceptor_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [usage_service_1.UsageService])
], UsageInterceptor);
//# sourceMappingURL=usage.interceptor.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"usage.interceptor.js","sourceRoot":"","sources":["../../src/usage/usage.interceptor.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export declare class UsageModule {
}
+28
View File
@@ -0,0 +1,28 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageModule = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const usage_entity_1 = require("./usage.entity");
const usage_service_1 = require("./usage.service");
const usage_controller_1 = require("./usage.controller");
let UsageModule = class UsageModule {
};
exports.UsageModule = UsageModule;
exports.UsageModule = UsageModule = __decorate([
(0, common_1.Module)({
imports: [
typeorm_1.TypeOrmModule.forFeature([usage_entity_1.UsageLog]),
],
providers: [usage_service_1.UsageService],
controllers: [usage_controller_1.UsageController],
exports: [usage_service_1.UsageService],
})
], UsageModule);
//# sourceMappingURL=usage.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"usage.module.js","sourceRoot":"","sources":["../../src/usage/usage.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,iDAA0C;AAC1C,mDAA+C;AAC/C,yDAAqD;AAU9C,IAAM,WAAW,GAAjB,MAAM,WAAW;CAAG,CAAA;AAAd,kCAAW;sBAAX,WAAW;IARvB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,uBAAQ,CAAC,CAAC;SACrC;QACD,SAAS,EAAE,CAAC,4BAAY,CAAC;QACzB,WAAW,EAAE,CAAC,kCAAe,CAAC;QAC9B,OAAO,EAAE,CAAC,4BAAY,CAAC;KACxB,CAAC;GACW,WAAW,CAAG"}
+39
View File
@@ -0,0 +1,39 @@
import { Repository } from 'typeorm';
import { UsageLog } from './usage.entity';
import { RedisService } from '../common/redis.service';
export declare class UsageService {
private usageRepository;
private redisService;
private readonly logger;
constructor(usageRepository: Repository<UsageLog>, redisService: RedisService);
recordRequest(data: {
tenantId: string;
apiKeyId: string;
endpoint: string;
method: string;
statusCode: number;
responseTimeMs: number;
userAgent?: string;
ipAddress?: string;
}): Promise<void>;
getMonthlyUsage(tenantId: string): Promise<number>;
checkQuota(tenantId: string, limit: number): Promise<{
allowed: boolean;
used: number;
}>;
checkRateLimit(tenantId: string, limit: number): Promise<{
allowed: boolean;
used: number;
}>;
getUsageHistory(tenantId: string, days?: number): Promise<any>;
getUsageSummary(tenantId: string, plan?: string): Promise<{
monthlyUsage: number;
monthlyLimit: any;
totalToday: number;
avgLatency: number;
successRate: number;
lastUpdated: Date;
}>;
private getMonthlyKey;
private getRateLimitKey;
}
+135
View File
@@ -0,0 +1,135 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var UsageService_1;
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const usage_entity_1 = require("./usage.entity");
const redis_service_1 = require("../common/redis.service");
let UsageService = UsageService_1 = class UsageService {
usageRepository;
redisService;
logger = new common_1.Logger(UsageService_1.name);
constructor(usageRepository, redisService) {
this.usageRepository = usageRepository;
this.redisService = redisService;
}
async recordRequest(data) {
this.usageRepository.save(data).catch((err) => {
this.logger.error(`Failed to save usage log to DB: ${err.message}`);
});
const monthKey = this.getMonthlyKey(data.tenantId);
try {
await this.redisService.getClient().incr(monthKey);
}
catch (err) {
this.logger.error(`Failed to increment Redis usage counter: ${err.message}`);
}
}
async getMonthlyUsage(tenantId) {
const key = this.getMonthlyKey(tenantId);
const val = await this.redisService.getClient().get(key);
return val ? parseInt(val, 10) : 0;
}
async checkQuota(tenantId, limit) {
const used = await this.getMonthlyUsage(tenantId);
return {
allowed: used < limit,
used,
};
}
async checkRateLimit(tenantId, limit) {
const key = this.getRateLimitKey(tenantId);
try {
const client = this.redisService.getClient();
const usedRaw = await client.get(key);
const used = usedRaw ? parseInt(usedRaw, 10) : 0;
if (used >= limit) {
return { allowed: false, used };
}
const multi = client.multi();
multi.incr(key);
if (!usedRaw) {
multi.expire(key, 60);
}
await multi.exec();
return { allowed: true, used: used + 1 };
}
catch (err) {
this.logger.error(`Rate limit check failed for ${tenantId}: ${err.message}`);
return { allowed: true, used: 0 };
}
}
async getUsageHistory(tenantId, days = 30) {
return this.usageRepository
.createQueryBuilder('usage')
.select("DATE_TRUNC('day', usage.createdAt)", 'date')
.addSelect('COUNT(*)', 'count')
.where('usage.tenantId = :tenantId', { tenantId })
.andWhere("usage.createdAt >= NOW() - (:days || ' days')::INTERVAL", { days: days.toString() })
.groupBy("DATE_TRUNC('day', usage.createdAt)")
.orderBy('date', 'ASC')
.getRawMany();
}
async getUsageSummary(tenantId, plan = 'FREE') {
const monthlyUsage = await this.getMonthlyUsage(tenantId);
const QUOTA_LIMITS = {
'FREE': 8000,
'PRO': 50000,
'ENTERPRISE': 1000000
};
const monthlyLimit = QUOTA_LIMITS[plan] || 8000;
const stats = await this.usageRepository
.createQueryBuilder('usage')
.select('COUNT(*)', 'totalToday')
.addSelect('AVG(usage.responseTimeMs)', 'avgLatency')
.addSelect('COUNT(CASE WHEN usage.statusCode >= 200 AND usage.statusCode < 300 THEN 1 END)', 'successCount')
.where('usage.tenantId = :tenantId', { tenantId })
.andWhere('usage.createdAt >= CURRENT_DATE')
.getRawOne();
const totalToday = parseInt(stats.totalToday || '0', 10);
const avgLatency = Math.round(parseFloat(stats.avgLatency || '0'));
const successRate = totalToday > 0
? Math.round((parseInt(stats.successCount || '0', 10) / totalToday) * 100)
: 100;
return {
monthlyUsage,
monthlyLimit,
totalToday,
avgLatency,
successRate,
lastUpdated: new Date()
};
}
getMonthlyKey(tenantId) {
const now = new Date();
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
return `usage:${tenantId}:${yearMonth}`;
}
getRateLimitKey(tenantId) {
const now = new Date();
const window = `${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}${now.getMinutes()}`;
return `ratelimit:${tenantId}:${window}`;
}
};
exports.UsageService = UsageService;
exports.UsageService = UsageService = UsageService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(usage_entity_1.UsageLog)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, redis_service_1.RedisService])
], UsageService);
//# sourceMappingURL=usage.service.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"usage.service.js","sourceRoot":"","sources":["../../src/usage/usage.service.ts"],"names":[],"mappings":"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"}