190 lines
5.2 KiB
TypeScript
Executable File
190 lines
5.2 KiB
TypeScript
Executable File
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'
|
||
items: {
|
||
matchId: string;
|
||
selection: string; // 'MS 1', '2.5 UST'
|
||
odd: number;
|
||
}[];
|
||
isPublic?: boolean;
|
||
}
|
||
|
||
export interface UserStatsDto {
|
||
totalCoupons: number;
|
||
wonCoupons: number;
|
||
winRate: number; // Percentage
|
||
totalInvested: number; // Unit based (1 unit per coupon)
|
||
totalReturn: number;
|
||
roi: number; // Return on Investment %
|
||
}
|
||
|
||
@Injectable()
|
||
export class UserCouponService {
|
||
private readonly logger = new Logger(UserCouponService.name);
|
||
|
||
constructor(private readonly prisma: PrismaService) {}
|
||
|
||
/**
|
||
* Kullanıcı için yeni bir kupon oluşturur ve kaydeder.
|
||
*/
|
||
async createCoupon(user: User, dto: CreateCouponDto): Promise<UserCoupon> {
|
||
const totalOdds = dto.items.reduce((acc, item) => acc * item.odd, 1);
|
||
|
||
const coupon = await this.prisma.userCoupon.create({
|
||
data: {
|
||
userId: user.id,
|
||
strategy: dto.strategy,
|
||
totalOdds: parseFloat(totalOdds.toFixed(2)),
|
||
isPublic: dto.isPublic || false,
|
||
status: "PENDING",
|
||
couponItems: {
|
||
create: dto.items.map((item) => ({
|
||
matchId: item.matchId,
|
||
selection: item.selection,
|
||
oddAtTime: item.odd,
|
||
})),
|
||
},
|
||
},
|
||
include: {
|
||
couponItems: true,
|
||
},
|
||
});
|
||
|
||
this.logger.log(
|
||
`Coupon created for user ${user.email} with odds ${totalOdds}`,
|
||
);
|
||
return coupon;
|
||
}
|
||
|
||
/**
|
||
* Bekleyen kuponların sonuçlarını kontrol eder ve günceller.
|
||
* Bu metod bir Cron Job tarafından periyodik olarak çağrılmalıdır.
|
||
*/
|
||
async updatePendingCoupons(): Promise<void> {
|
||
// Sadece bitmiş (FT) maçları içeren PENDING kuponları çek
|
||
const pendingCoupons = await this.prisma.userCoupon.findMany({
|
||
where: { status: "PENDING" },
|
||
include: {
|
||
couponItems: {
|
||
include: { match: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
for (const coupon of pendingCoupons) {
|
||
let isCouponWon = true;
|
||
let isCouponLost = false;
|
||
let allMatchesFinished = true;
|
||
|
||
for (const item of coupon.couponItems) {
|
||
if (item.match.status !== "FT") {
|
||
allMatchesFinished = false;
|
||
break; // Henüz bitmemiş maç var, kuponu güncelleme
|
||
}
|
||
|
||
const isItemWon = this.checkSelection(item.selection, item.match);
|
||
|
||
// Sonucu item bazında güncelle
|
||
if (item.isCorrect !== isItemWon) {
|
||
await this.prisma.userCouponItem.update({
|
||
where: { id: item.id },
|
||
data: { isCorrect: isItemWon },
|
||
});
|
||
}
|
||
|
||
if (!isItemWon) {
|
||
isCouponLost = true;
|
||
isCouponWon = false;
|
||
}
|
||
}
|
||
|
||
if (isCouponLost) {
|
||
await this.prisma.userCoupon.update({
|
||
where: { id: coupon.id },
|
||
data: { status: "LOST" },
|
||
});
|
||
} else if (allMatchesFinished && isCouponWon) {
|
||
await this.prisma.userCoupon.update({
|
||
where: { id: coupon.id },
|
||
data: { status: "WON" },
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Basit bir kural seti ile bahsin tutup tutmadığını kontrol eder.
|
||
* Gerçek dünyada bu daha karmaşık bir 'BetSettlementService' olmalıdır.
|
||
*/
|
||
private checkSelection(selection: string, match: Match): boolean {
|
||
const home = match.scoreHome ?? 0;
|
||
const away = match.scoreAway ?? 0;
|
||
const total = home + away;
|
||
|
||
switch (selection) {
|
||
case "MS 1":
|
||
return home > away;
|
||
case "MS X":
|
||
return home === away;
|
||
case "MS 2":
|
||
return away > home;
|
||
case "1.5 UST":
|
||
return total > 1.5;
|
||
case "2.5 UST":
|
||
return total > 2.5;
|
||
case "3.5 UST":
|
||
return total > 3.5;
|
||
case "2.5 ALT":
|
||
return total < 2.5;
|
||
case "KG VAR":
|
||
return home > 0 && away > 0;
|
||
case "KG YOK":
|
||
return home === 0 || away === 0;
|
||
default:
|
||
return false; // Bilinmeyen market
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Kullanıcının bahis performans istatistiklerini getirir.
|
||
*/
|
||
async getUserStatistics(userId: string): Promise<UserStatsDto> {
|
||
const coupons = await this.prisma.userCoupon.findMany({
|
||
where: {
|
||
userId,
|
||
status: { in: ["WON", "LOST"] },
|
||
},
|
||
});
|
||
|
||
const totalCoupons = coupons.length;
|
||
if (totalCoupons === 0) {
|
||
return {
|
||
totalCoupons: 0,
|
||
wonCoupons: 0,
|
||
winRate: 0,
|
||
totalInvested: 0,
|
||
totalReturn: 0,
|
||
roi: 0,
|
||
};
|
||
}
|
||
|
||
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;
|
||
const roi = ((totalReturn - totalInvested) / totalInvested) * 100;
|
||
|
||
return {
|
||
totalCoupons,
|
||
wonCoupons: wonCoupons.length,
|
||
winRate: parseFloat(winRate.toFixed(2)),
|
||
totalInvested,
|
||
totalReturn: parseFloat(totalReturn.toFixed(2)),
|
||
roi: parseFloat(roi.toFixed(2)),
|
||
};
|
||
}
|
||
}
|