feat(admin,api,player): settlement stats, team crests, MS fields and list bet summary

This commit is contained in:
2026-06-04 17:30:48 +08:00
parent cc737e2924
commit 9fcee31a9a
27 changed files with 2296 additions and 427 deletions

View File

@@ -294,6 +294,141 @@ export class SettlementService {
return { success: true, batchId: batchId.toString() };
}
async getMatchBetStats(matchId: bigint) {
const match = await this.prisma.match.findFirst({
where: { id: matchId, deletedAt: null },
});
if (!match) throw new NotFoundException('Match not found');
const legs = await this.prisma.betSelection.findMany({
where: { matchId },
include: {
bet: {
select: {
id: true,
betNo: true,
betType: true,
stake: true,
status: true,
settlementStatus: true,
potentialReturn: true,
actualReturn: true,
placedAt: true,
user: { select: { username: true } },
},
},
},
orderBy: [{ marketType: 'asc' }, { sortOrder: 'asc' }, { id: 'asc' }],
});
const betById = new Map<string, (typeof legs)[0]['bet']>();
for (const leg of legs) {
betById.set(leg.betId.toString(), leg.bet);
}
let totalStake = new Decimal(0);
let totalPotential = new Decimal(0);
let singleBets = 0;
let parlayBets = 0;
const statusCounts: Record<string, number> = {};
for (const bet of betById.values()) {
totalStake = totalStake.add(bet.stake);
if (bet.potentialReturn) {
totalPotential = totalPotential.add(bet.potentialReturn);
}
if (bet.betType === 'SINGLE') singleBets += 1;
else if (bet.betType === 'PARLAY') parlayBets += 1;
statusCounts[bet.status] = (statusCounts[bet.status] ?? 0) + 1;
}
type SelAgg = {
marketType: string;
period: string | null;
selectionName: string;
selectionId: string;
legCount: number;
singleStake: Decimal;
parlayLegCount: number;
};
const selMap = new Map<string, SelAgg>();
for (const leg of legs) {
const key = `${leg.marketId.toString()}:${leg.selectionId.toString()}`;
let row = selMap.get(key);
if (!row) {
row = {
marketType: leg.marketType,
period: leg.period,
selectionName: leg.selectionNameSnapshot,
selectionId: leg.selectionId.toString(),
legCount: 0,
singleStake: new Decimal(0),
parlayLegCount: 0,
};
selMap.set(key, row);
}
row.legCount += 1;
if (leg.bet.betType === 'SINGLE') {
row.singleStake = row.singleStake.add(leg.bet.stake);
} else if (leg.bet.betType === 'PARLAY') {
row.parlayLegCount += 1;
}
}
const bySelection = Array.from(selMap.values())
.map((r) => ({
marketType: r.marketType,
period: r.period,
selectionName: r.selectionName,
selectionId: r.selectionId,
legCount: r.legCount,
singleStake: r.singleStake.toString(),
parlayLegCount: r.parlayLegCount,
}))
.sort((a, b) => {
const mk = a.marketType.localeCompare(b.marketType);
if (mk !== 0) return mk;
return a.selectionName.localeCompare(b.selectionName);
});
const bets = Array.from(legs)
.map((leg) => ({
id: leg.bet.id.toString(),
betNo: leg.bet.betNo,
username: leg.bet.user.username,
betType: leg.bet.betType,
status: leg.bet.status,
settlementStatus: leg.bet.settlementStatus,
stake: leg.bet.stake.toString(),
potentialReturn: leg.bet.potentialReturn?.toString() ?? null,
actualReturn: leg.bet.actualReturn.toString(),
placedAt: leg.bet.placedAt.toISOString(),
marketType: leg.marketType,
period: leg.period,
selectionName: leg.selectionNameSnapshot,
odds: leg.odds.toString(),
}))
.sort(
(a, b) =>
new Date(b.placedAt).getTime() - new Date(a.placedAt).getTime(),
);
return {
summary: {
totalBets: betById.size,
singleBets,
parlayBets,
totalStake: totalStake.toString(),
totalPotentialReturn: totalPotential.toString(),
statusCounts,
legCount: legs.length,
},
bySelection,
bets,
};
}
async voidMatchBets(matchId: bigint) {
const bets = await this.prisma.bet.findMany({
where: { status: 'PENDING', selections: { some: { matchId } } },