275 lines
7.8 KiB
TypeScript
Executable File
275 lines
7.8 KiB
TypeScript
Executable File
import {
|
||
Controller,
|
||
Post,
|
||
Get,
|
||
Body,
|
||
Query,
|
||
HttpCode,
|
||
HttpStatus,
|
||
UseGuards,
|
||
Req,
|
||
Logger,
|
||
} 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";
|
||
import {
|
||
UserCouponService,
|
||
CreateCouponDto,
|
||
} from "./services/user-coupon.service";
|
||
import {
|
||
AnalyzeMatchDto,
|
||
DailyBankoDto,
|
||
SuggestCouponDto,
|
||
FrequencyCouponDto,
|
||
} 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")
|
||
export class CouponsController {
|
||
private readonly logger = new Logger(CouponsController.name);
|
||
|
||
constructor(
|
||
private readonly couponsService: CouponsService,
|
||
private readonly smartCouponService: SmartCouponService,
|
||
private readonly userCouponService: UserCouponService,
|
||
private readonly matchesService: MatchesService,
|
||
) {}
|
||
|
||
/**
|
||
* POST /coupon/analyze-match
|
||
* Analyze a single match with V20+ single-match package
|
||
*/
|
||
@Post("analyze-match")
|
||
@Public()
|
||
@HttpCode(HttpStatus.OK)
|
||
@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: true, data: analysis };
|
||
}
|
||
|
||
/**
|
||
* POST /coupon/analyze (alias for /analyze-match - frontend compatibility)
|
||
*/
|
||
@Post("analyze")
|
||
@Public()
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({
|
||
summary: "Analyze single match with V20 model (alias)",
|
||
deprecated: true,
|
||
})
|
||
async analyzeMatchAlias(@Body() dto: AnalyzeMatchDto) {
|
||
return this.analyzeMatch(dto);
|
||
}
|
||
|
||
/**
|
||
* POST /coupon
|
||
* Alias for /coupon/create - frontend compatibility
|
||
*/
|
||
@Post()
|
||
@UseGuards(JwtAuthGuard)
|
||
@ApiBearerAuth()
|
||
@HttpCode(HttpStatus.CREATED)
|
||
@ApiOperation({ summary: "Create and save a user coupon (alias)" })
|
||
async createCouponAlias(@Body() dto: CreateCouponDto, @Req() req: any) {
|
||
return this.createCoupon(dto, req);
|
||
}
|
||
|
||
/**
|
||
* POST /coupon/daily-banko
|
||
* Generate a high-confidence banko combo (2 matches)
|
||
*/
|
||
@Post("daily-banko")
|
||
@Public()
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({
|
||
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)
|
||
let candidateMatches = dto.matchIds || [];
|
||
if (candidateMatches.length === 0) {
|
||
candidateMatches = await this.matchesService.findUpcomingMatches(
|
||
Sport.FOOTBALL,
|
||
20,
|
||
);
|
||
this.logger.debug(
|
||
`Auto-fetched ${candidateMatches.length} matches for daily-banko`,
|
||
);
|
||
} else {
|
||
candidateMatches = await this.matchesService.filterUpcomingMatchIds(
|
||
candidateMatches,
|
||
Sport.FOOTBALL,
|
||
);
|
||
this.logger.debug(
|
||
`Sanitized candidate matches for daily-banko: ${candidateMatches.length}`,
|
||
);
|
||
}
|
||
|
||
if (candidateMatches.length === 0) {
|
||
return {
|
||
success: false,
|
||
message: "Kupon için uygun, henüz başlamamış maç bulunamadı.",
|
||
};
|
||
}
|
||
|
||
const coupon =
|
||
await this.smartCouponService.generateDailyBankoCoupon(candidateMatches);
|
||
if (!coupon) {
|
||
return {
|
||
success: false,
|
||
message: "Kriterlere uygun (80%+ güvenli) yeterli maç bulunamadı.",
|
||
};
|
||
}
|
||
return { success: true, data: coupon };
|
||
}
|
||
|
||
/**
|
||
* POST /coupon/suggest
|
||
* Generate Smart Coupon
|
||
*/
|
||
@Post("suggest")
|
||
@Public()
|
||
@HttpCode(HttpStatus.OK)
|
||
@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 || [];
|
||
if (candidateMatches.length === 0) {
|
||
candidateMatches = await this.matchesService.findUpcomingMatches(
|
||
Sport.FOOTBALL,
|
||
20,
|
||
);
|
||
this.logger.debug(
|
||
`Auto-fetched ${candidateMatches.length} matches for suggest`,
|
||
);
|
||
} else {
|
||
candidateMatches = await this.matchesService.filterUpcomingMatchIds(
|
||
candidateMatches,
|
||
Sport.FOOTBALL,
|
||
);
|
||
this.logger.debug(
|
||
`Sanitized candidate matches for suggest: ${candidateMatches.length}`,
|
||
);
|
||
}
|
||
|
||
if (candidateMatches.length === 0) {
|
||
return {
|
||
success: false,
|
||
message: "Tahmin için uygun, henüz başlamamış maç bulunamadı.",
|
||
};
|
||
}
|
||
|
||
const coupon = await this.smartCouponService.getSmartCoupon(
|
||
candidateMatches,
|
||
dto.strategy,
|
||
{
|
||
maxMatches: dto.maxMatches,
|
||
minConfidence: dto.minConfidence,
|
||
},
|
||
);
|
||
if (!coupon) {
|
||
return { success: false, message: "Kupon oluşturulamadı." };
|
||
}
|
||
return { success: true, data: coupon };
|
||
}
|
||
|
||
/**
|
||
* POST /coupon/frequency-coupon
|
||
* Generate a frequency-based parlay coupon (Conditional Frequency Engine)
|
||
*/
|
||
@Post("frequency-coupon")
|
||
@Public()
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({
|
||
summary: "Generate frequency-based parlay coupon",
|
||
description:
|
||
"Scans upcoming matches, applies conditional frequency analysis " +
|
||
"(team odds-band performance), and builds 2-5 match combos with +EV calculation.",
|
||
})
|
||
@ApiResponse({ status: 200, description: "Frequency coupon generated" })
|
||
async getFrequencyCoupon(@Body() dto: FrequencyCouponDto) {
|
||
const coupon = await this.smartCouponService.generateFrequencyBasedCoupon({
|
||
matchIds: dto.matchIds,
|
||
maxMatches: dto.maxMatches,
|
||
minSignal: dto.minSignal,
|
||
markets: dto.markets,
|
||
});
|
||
|
||
if (!coupon || coupon.bets.length === 0) {
|
||
return {
|
||
success: false,
|
||
message:
|
||
"Frekans analizine uygun yeterli maç bulunamadı. " +
|
||
"minSignal değerini düşürmeyi veya daha fazla maç beklemeyi deneyin.",
|
||
data: coupon,
|
||
};
|
||
}
|
||
|
||
return { success: true, data: coupon };
|
||
}
|
||
|
||
// ============================================
|
||
// USER COUPON ENDPOINTS
|
||
// ============================================
|
||
|
||
/**
|
||
* POST /coupon/create
|
||
* Save a user generated coupon
|
||
*/
|
||
@Post("create")
|
||
@UseGuards(JwtAuthGuard)
|
||
@ApiBearerAuth()
|
||
@HttpCode(HttpStatus.CREATED)
|
||
@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);
|
||
return { success: true, data: coupon };
|
||
}
|
||
|
||
/**
|
||
* GET /coupon/my-stats
|
||
* Get user betting statistics (ROI, Win Rate)
|
||
*/
|
||
@Get("my-stats")
|
||
@UseGuards(JwtAuthGuard)
|
||
@ApiBearerAuth()
|
||
@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 };
|
||
}
|
||
|
||
/**
|
||
* GET /coupon/history
|
||
* Get coupon history (Public/System coupons)
|
||
*/
|
||
@Get("history")
|
||
@ApiBearerAuth()
|
||
@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,
|
||
);
|
||
return { success: true, data: results };
|
||
}
|
||
}
|