feat: implement billing, usage tracking, and administrative geocoding features in API with infrastructure updates
This commit is contained in:
Vendored
+135
@@ -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
|
||||
Reference in New Issue
Block a user