This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+36 -36
View File
@@ -9,31 +9,31 @@ import {
UseGuards,
Req,
Logger,
} from '@nestjs/common';
} from "@nestjs/common";
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiResponse,
} from '@nestjs/swagger';
import { CouponsService } from './coupons.service';
import { MatchesService } from '../matches/matches.service';
import { SmartCouponService } from './services/smart-coupon.service';
} from "@nestjs/swagger";
import { CouponsService } from "./coupons.service";
import { MatchesService } from "../matches/matches.service";
import { SmartCouponService } from "./services/smart-coupon.service";
import {
UserCouponService,
CreateCouponDto,
} from './services/user-coupon.service';
} from "./services/user-coupon.service";
import {
AnalyzeMatchDto,
DailyBankoDto,
SuggestCouponDto,
} from './dto/coupons-request.dto';
import { Public } from '../../common/decorators';
import { JwtAuthGuard } from '../auth/guards/auth.guards'; // Assuming standard guard
import { Sport } from '../matches/dto';
} from "./dto/coupons-request.dto";
import { Public } from "../../common/decorators";
import { JwtAuthGuard } from "../auth/guards/auth.guards"; // Assuming standard guard
import { Sport } from "../matches/dto";
@ApiTags('Coupon')
@Controller('coupon')
@ApiTags("Coupon")
@Controller("coupon")
export class CouponsController {
private readonly logger = new Logger(CouponsController.name);
@@ -48,15 +48,15 @@ export class CouponsController {
* POST /coupon/analyze-match
* Analyze a single match with V20+ single-match package
*/
@Post('analyze-match')
@Post("analyze-match")
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Analyze single match with V20 model' })
@ApiResponse({ status: 200, description: 'Match analysis' })
@ApiOperation({ summary: "Analyze single match with V20 model" })
@ApiResponse({ status: 200, description: "Match analysis" })
async analyzeMatch(@Body() dto: AnalyzeMatchDto) {
const analysis = await this.smartCouponService.analyzeMatch(dto.matchId);
if (!analysis) {
return { success: false, message: 'Analiz yapılamadı.' };
return { success: false, message: "Analiz yapılamadı." };
}
return { success: true, data: analysis };
}
@@ -64,11 +64,11 @@ export class CouponsController {
/**
* POST /coupon/analyze (alias for /analyze-match - frontend compatibility)
*/
@Post('analyze')
@Post("analyze")
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Analyze single match with V20 model (alias)',
summary: "Analyze single match with V20 model (alias)",
deprecated: true,
})
async analyzeMatchAlias(@Body() dto: AnalyzeMatchDto) {
@@ -83,7 +83,7 @@ export class CouponsController {
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create and save a user coupon (alias)' })
@ApiOperation({ summary: "Create and save a user coupon (alias)" })
async createCouponAlias(@Body() dto: CreateCouponDto, @Req() req: any) {
return this.createCoupon(dto, req);
}
@@ -92,11 +92,11 @@ export class CouponsController {
* POST /coupon/daily-banko
* Generate a high-confidence banko combo (2 matches)
*/
@Post('daily-banko')
@Post("daily-banko")
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Generate a high-confidence banko combo (2 matches)',
summary: "Generate a high-confidence banko combo (2 matches)",
})
async getDailyBanko(@Body() dto: DailyBankoDto) {
// If no match IDs provided, fetch from system (top 50 upcoming)
@@ -122,7 +122,7 @@ export class CouponsController {
if (candidateMatches.length === 0) {
return {
success: false,
message: 'Kupon için uygun, henüz başlamamış maç bulunamadı.',
message: "Kupon için uygun, henüz başlamamış maç bulunamadı.",
};
}
@@ -131,7 +131,7 @@ export class CouponsController {
if (!coupon) {
return {
success: false,
message: 'Kriterlere uygun (80%+ güvenli) yeterli maç bulunamadı.',
message: "Kriterlere uygun (80%+ güvenli) yeterli maç bulunamadı.",
};
}
return { success: true, data: coupon };
@@ -141,11 +141,11 @@ export class CouponsController {
* POST /coupon/suggest
* Generate Smart Coupon
*/
@Post('suggest')
@Post("suggest")
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Suggest Smart Coupon' })
@ApiResponse({ status: 200, description: 'Smart Coupon generated' })
@ApiOperation({ summary: "Suggest Smart Coupon" })
@ApiResponse({ status: 200, description: "Smart Coupon generated" })
async suggestCoupon(@Body() dto: SuggestCouponDto) {
// If no match IDs provided, fetch from system (top 50 upcoming)
let candidateMatches = dto.matchIds || [];
@@ -170,7 +170,7 @@ export class CouponsController {
if (candidateMatches.length === 0) {
return {
success: false,
message: 'Tahmin için uygun, henüz başlamamış maç bulunamadı.',
message: "Tahmin için uygun, henüz başlamamış maç bulunamadı.",
};
}
@@ -183,7 +183,7 @@ export class CouponsController {
},
);
if (!coupon) {
return { success: false, message: 'Kupon oluşturulamadı.' };
return { success: false, message: "Kupon oluşturulamadı." };
}
return { success: true, data: coupon };
}
@@ -196,11 +196,11 @@ export class CouponsController {
* POST /coupon/create
* Save a user generated coupon
*/
@Post('create')
@Post("create")
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create and save a user coupon' })
@ApiOperation({ summary: "Create and save a user coupon" })
async createCoupon(@Body() dto: CreateCouponDto, @Req() req: any) {
// req.user is populated by JwtAuthGuard
const coupon = await this.userCouponService.createCoupon(req.user, dto);
@@ -211,10 +211,10 @@ export class CouponsController {
* GET /coupon/my-stats
* Get user betting statistics (ROI, Win Rate)
*/
@Get('my-stats')
@Get("my-stats")
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get user betting statistics' })
@ApiOperation({ summary: "Get user betting statistics" })
async getUserStats(@Req() req: any) {
const stats = await this.userCouponService.getUserStatistics(req.user.id);
return { success: true, data: stats };
@@ -224,11 +224,11 @@ export class CouponsController {
* GET /coupon/history
* Get coupon history (Public/System coupons)
*/
@Get('history')
@Get("history")
@ApiBearerAuth()
@ApiOperation({ summary: 'Get coupon history' })
@ApiResponse({ status: 200, description: 'History retrieved' })
async getHistory(@Query('limit') limit?: string) {
@ApiOperation({ summary: "Get coupon history" })
@ApiResponse({ status: 200, description: "History retrieved" })
async getHistory(@Query("limit") limit?: string) {
// eslint-disable-next-line @typescript-eslint/await-thenable
const results = await this.couponsService.getCouponHistory(
Number(limit) || 10,
+8 -8
View File
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { CouponsController } from './coupons.controller';
import { SmartCouponService } from './services/smart-coupon.service';
import { UserCouponService } from './services/user-coupon.service';
import { CouponsService } from './coupons.service';
import { DatabaseModule } from '../../database/database.module';
import { ServicesModule } from '../../services/services.module';
import { MatchesModule } from '../matches/matches.module';
import { Module } from "@nestjs/common";
import { CouponsController } from "./coupons.controller";
import { SmartCouponService } from "./services/smart-coupon.service";
import { UserCouponService } from "./services/user-coupon.service";
import { CouponsService } from "./coupons.service";
import { DatabaseModule } from "../../database/database.module";
import { ServicesModule } from "../../services/services.module";
import { MatchesModule } from "../matches/matches.module";
@Module({
imports: [DatabaseModule, ServicesModule, MatchesModule],
+4 -4
View File
@@ -1,9 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { AiService } from '../../services/ai.service';
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../database/prisma.service";
import { AiService } from "../../services/ai.service";
// [REMOVED V16 IMPORTS]
export type RiskLevel = 'banko' | 'safe' | 'value';
export type RiskLevel = "banko" | "safe" | "value";
export interface CouponMatch {
matchId: string;
+14 -14
View File
@@ -8,19 +8,19 @@ import {
ArrayMaxSize,
Min,
Max,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
} from "class-validator";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export enum CouponStrategyEnum {
SAFE = 'SAFE',
BALANCED = 'BALANCED',
AGGRESSIVE = 'AGGRESSIVE',
VALUE = 'VALUE',
MIRACLE = 'MIRACLE',
SAFE = "SAFE",
BALANCED = "BALANCED",
AGGRESSIVE = "AGGRESSIVE",
VALUE = "VALUE",
MIRACLE = "MIRACLE",
}
export class AnalyzeMatchDto {
@ApiProperty({ description: 'Match ID to analyze' })
@ApiProperty({ description: "Match ID to analyze" })
@IsString()
@IsNotEmpty()
matchId: string;
@@ -28,8 +28,8 @@ export class AnalyzeMatchDto {
export class DailyBankoDto {
@ApiPropertyOptional({
description: 'Optional match IDs — system fetches if empty',
example: ['match-1', 'match-2'],
description: "Optional match IDs — system fetches if empty",
example: ["match-1", "match-2"],
})
@IsOptional()
@IsArray()
@@ -40,8 +40,8 @@ export class DailyBankoDto {
export class SuggestCouponDto {
@ApiPropertyOptional({
description: 'Match IDs — system fetches if empty',
example: ['match-1', 'match-2'],
description: "Match IDs — system fetches if empty",
example: ["match-1", "match-2"],
})
@IsOptional()
@IsArray()
@@ -57,7 +57,7 @@ export class SuggestCouponDto {
@IsEnum(CouponStrategyEnum)
strategy?: CouponStrategyEnum;
@ApiPropertyOptional({ description: 'Maximum matches in coupon', example: 5 })
@ApiPropertyOptional({ description: "Maximum matches in coupon", example: 5 })
@IsOptional()
@IsNumber()
@Min(1)
@@ -65,7 +65,7 @@ export class SuggestCouponDto {
maxMatches?: number;
@ApiPropertyOptional({
description: 'Minimum confidence threshold (0-100)',
description: "Minimum confidence threshold (0-100)",
example: 60,
})
@IsOptional()
@@ -1,10 +1,10 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { GeminiService } from '../../gemini/gemini.service';
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import axios from "axios";
import { GeminiService } from "../../gemini/gemini.service";
export type PredictionRiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'EXTREME';
export type PredictionDataQuality = 'HIGH' | 'MEDIUM' | 'LOW';
export type BetGrade = 'A' | 'B' | 'C' | 'PASS';
export type PredictionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
export type PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
export type BetGrade = "A" | "B" | "C" | "PASS";
export interface PredictionPickRow {
market: string;
@@ -128,7 +128,7 @@ export class SmartCouponService {
private readonly aiEngineUrl: string;
constructor(private readonly geminiService: GeminiService) {
this.aiEngineUrl = process.env.AI_ENGINE_URL || 'http://ai-engine:8000';
this.aiEngineUrl = process.env.AI_ENGINE_URL || "http://ai-engine:8000";
}
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
@@ -147,7 +147,7 @@ export class SmartCouponService {
);
}
throw new HttpException(
'AI analyze failed',
"AI analyze failed",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
@@ -168,7 +168,7 @@ export class SmartCouponService {
const result = await this.geminiService.generateText(
JSON.stringify(prediction, null, 2),
{
model: 'gemini-2.0-flash',
model: "gemini-2.0-flash",
temperature: 0.7,
maxTokens: 600,
systemPrompt: MATCH_COMMENTARY_SYSTEM_PROMPT,
@@ -176,7 +176,7 @@ export class SmartCouponService {
);
return result.text || null;
} catch (error) {
this.logger.warn('AI commentary generation failed, skipping', error);
this.logger.warn("AI commentary generation failed, skipping", error);
return null;
}
}
@@ -188,7 +188,7 @@ export class SmartCouponService {
return null;
}
return this.getSmartCoupon(matchIds, 'SAFE', {
return this.getSmartCoupon(matchIds, "SAFE", {
maxMatches: 2,
minConfidence: 78,
});
@@ -197,11 +197,11 @@ export class SmartCouponService {
async getSmartCoupon(
matchIds: string[],
strategy:
| 'SAFE'
| 'BALANCED'
| 'AGGRESSIVE'
| 'VALUE'
| 'MIRACLE' = 'BALANCED',
| "SAFE"
| "BALANCED"
| "AGGRESSIVE"
| "VALUE"
| "MIRACLE" = "BALANCED",
options: { maxMatches?: number; minConfidence?: number } = {},
): Promise<SmartCouponResult> {
try {
@@ -216,7 +216,7 @@ export class SmartCouponService {
);
return response.data;
} catch (error) {
this.logger.error('Failed to generate smart coupon', error);
this.logger.error("Failed to generate smart coupon", error);
if (axios.isAxiosError(error)) {
const detail = error.response?.data?.detail || error.message;
throw new HttpException(
@@ -225,7 +225,7 @@ export class SmartCouponService {
);
}
throw new HttpException(
'Coupon generation failed',
"Coupon generation failed",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { User, UserCoupon, Match } from '@prisma/client';
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../database/prisma.service";
import { User, UserCoupon, Match } from "@prisma/client";
export class CreateCouponDto {
strategy: string; // 'SAFE', 'VALUE', 'CUSTOM'
@@ -39,7 +39,7 @@ export class UserCouponService {
strategy: dto.strategy,
totalOdds: parseFloat(totalOdds.toFixed(2)),
isPublic: dto.isPublic || false,
status: 'PENDING',
status: "PENDING",
couponItems: {
create: dto.items.map((item) => ({
matchId: item.matchId,
@@ -66,7 +66,7 @@ export class UserCouponService {
async updatePendingCoupons(): Promise<void> {
// Sadece bitmiş (FT) maçları içeren PENDING kuponları çek
const pendingCoupons = await this.prisma.userCoupon.findMany({
where: { status: 'PENDING' },
where: { status: "PENDING" },
include: {
couponItems: {
include: { match: true },
@@ -80,7 +80,7 @@ export class UserCouponService {
let allMatchesFinished = true;
for (const item of coupon.couponItems) {
if (item.match.status !== 'FT') {
if (item.match.status !== "FT") {
allMatchesFinished = false;
break; // Henüz bitmemiş maç var, kuponu güncelleme
}
@@ -104,12 +104,12 @@ export class UserCouponService {
if (isCouponLost) {
await this.prisma.userCoupon.update({
where: { id: coupon.id },
data: { status: 'LOST' },
data: { status: "LOST" },
});
} else if (allMatchesFinished && isCouponWon) {
await this.prisma.userCoupon.update({
where: { id: coupon.id },
data: { status: 'WON' },
data: { status: "WON" },
});
}
}
@@ -125,23 +125,23 @@ export class UserCouponService {
const total = home + away;
switch (selection) {
case 'MS 1':
case "MS 1":
return home > away;
case 'MS X':
case "MS X":
return home === away;
case 'MS 2':
case "MS 2":
return away > home;
case '1.5 UST':
case "1.5 UST":
return total > 1.5;
case '2.5 UST':
case "2.5 UST":
return total > 2.5;
case '3.5 UST':
case "3.5 UST":
return total > 3.5;
case '2.5 ALT':
case "2.5 ALT":
return total < 2.5;
case 'KG VAR':
case "KG VAR":
return home > 0 && away > 0;
case 'KG YOK':
case "KG YOK":
return home === 0 || away === 0;
default:
return false; // Bilinmeyen market
@@ -155,7 +155,7 @@ export class UserCouponService {
const coupons = await this.prisma.userCoupon.findMany({
where: {
userId,
status: { in: ['WON', 'LOST'] },
status: { in: ["WON", "LOST"] },
},
});
@@ -171,7 +171,7 @@ export class UserCouponService {
};
}
const wonCoupons = coupons.filter((c) => c.status === 'WON');
const wonCoupons = coupons.filter((c) => c.status === "WON");
const totalInvested = totalCoupons; // Her kupona 1 birim yatırıldığını varsayıyoruz
const totalReturn = wonCoupons.reduce((acc, c) => acc + c.totalOdds, 0);
const winRate = (wonCoupons.length / totalCoupons) * 100;