This commit is contained in:
2026-05-10 10:37:45 +03:00
parent 4f7090e2d9
commit c525b12dfd
32 changed files with 2374 additions and 209 deletions
@@ -21,12 +21,17 @@ import {
GeneratePredictionDto,
SmartCouponRequestDto,
} from "./dto/predictions-request.dto";
import { Public } from "src/common/decorators";
import { CurrentUser } from "src/common/decorators";
import { AnalysisService } from "../analysis/analysis.service";
import { ForbiddenException } from "@nestjs/common";
@ApiTags("Predictions")
@Controller("predictions")
export class PredictionsController {
constructor(private readonly predictionsService: PredictionsService) {}
constructor(
private readonly predictionsService: PredictionsService,
private readonly analysisService: AnalysisService,
) {}
/**
* GET /predictions/health
@@ -93,7 +98,6 @@ export class PredictionsController {
* Get prediction for a specific match
*/
@Get(":matchId")
@Public()
@ApiOperation({ summary: "Get prediction for a specific match" })
@ApiParam({ name: "matchId", description: "Match ID" })
@ApiResponse({
@@ -103,11 +107,23 @@ export class PredictionsController {
type: MatchPredictionDto,
})
@ApiResponse({ status: 404, description: "Match not found" })
@ApiResponse({ status: 403, description: "Daily limit exceeded" })
async getPrediction(
@Param("matchId") matchId: string,
@CurrentUser() user: any,
): Promise<MatchPredictionDto> {
const canProceed = await this.analysisService.checkUsageLimit(
user.id,
false,
1,
);
if (!canProceed) {
throw new ForbiddenException("ANALYSIS_LIMIT_EXCEEDED");
}
const cached = await this.predictionsService.getCachedPrediction(matchId);
if (cached) {
await this.analysisService.recordUsage(user.id, false);
return cached;
}
@@ -115,9 +131,10 @@ export class PredictionsController {
const prediction = await this.predictionsService.getPredictionById(matchId);
if (!prediction) {
throw new NotFoundException(`Match not found: ${matchId}`);
throw new NotFoundException("MATCH_NOT_FOUND");
}
await this.analysisService.recordUsage(user.id, false);
return prediction;
}
@@ -129,17 +146,29 @@ export class PredictionsController {
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Generate prediction with provided match data" })
@ApiResponse({ status: 200, type: MatchPredictionDto })
@ApiResponse({ status: 403, description: "Daily limit exceeded" })
async generatePrediction(
@CurrentUser() user: any,
@Body() dto: GeneratePredictionDto,
): Promise<MatchPredictionDto> {
const canProceed = await this.analysisService.checkUsageLimit(
user.id,
false,
1,
);
if (!canProceed) {
throw new ForbiddenException("ANALYSIS_LIMIT_EXCEEDED");
}
const prediction = await this.predictionsService.getPredictionWithData({
matchId: dto.matchId,
});
if (!prediction) {
throw new NotFoundException("Failed to generate prediction");
throw new NotFoundException("PREDICTION_GENERATION_FAILED");
}
await this.analysisService.recordUsage(user.id, false);
return prediction;
}
@@ -157,7 +186,20 @@ export class PredictionsController {
description: "Smart coupon generated successfully",
schema: { type: "object" },
})
async generateSmartCoupon(@Body() dto: SmartCouponRequestDto): Promise<any> {
@ApiResponse({ status: 403, description: "Daily limit exceeded" })
async generateSmartCoupon(
@CurrentUser() user: any,
@Body() dto: SmartCouponRequestDto,
): Promise<any> {
const canProceed = await this.analysisService.checkUsageLimit(
user.id,
true,
dto.matchIds?.length || 1,
);
if (!canProceed) {
throw new ForbiddenException("COUPON_LIMIT_EXCEEDED");
}
const coupon = await this.predictionsService.getSmartCoupon(
dto.matchIds,
dto.strategy || "BALANCED",
@@ -168,9 +210,10 @@ export class PredictionsController {
);
if (!coupon) {
throw new NotFoundException("Failed to generate Smart Coupon");
throw new NotFoundException("SMART_COUPON_GENERATION_FAILED");
}
await this.analysisService.recordUsage(user.id, true);
return coupon;
}
}
@@ -10,6 +10,7 @@ import { PredictionsQueue } from "./queues/predictions.queue";
import { PredictionsProcessor } from "./queues/predictions.processor";
import { PREDICTIONS_QUEUE } from "./queues/predictions.types";
import { FeederModule } from "../feeder/feeder.module";
import { AnalysisModule } from "../analysis/analysis.module";
const redisEnabled = process.env.REDIS_ENABLED === "true";
@@ -25,6 +26,7 @@ const redisEnabled = process.env.REDIS_ENABLED === "true";
: []),
MatchesModule,
FeederModule,
AnalysisModule,
],
controllers: [PredictionsController],
providers: [
@@ -1354,8 +1354,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
}
private extractCooldownMs(detail: unknown): number {
if (detail && typeof detail === "object" && "cooldownRemainingMs" in detail) {
return Number((detail as Record<string, unknown>).cooldownRemainingMs) || 0;
if (
detail &&
typeof detail === "object" &&
"cooldownRemainingMs" in detail
) {
return (
Number((detail as Record<string, unknown>).cooldownRemainingMs) || 0
);
}
if (typeof detail === "string") {