重构
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { WalletService } from '../ledger/wallet.service';
|
||||
import { FundsPostingService } from '../ledger/funds-posting.service';
|
||||
import { AgentsService } from '../agent/agents.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
calculatePayout,
|
||||
calculateParlayPayout,
|
||||
ScoreInput,
|
||||
MatchStatsInput,
|
||||
SelectionResult,
|
||||
} from './domain/settlement-calculator';
|
||||
import {
|
||||
@@ -18,6 +20,15 @@ import {
|
||||
} from './domain/settlement-helpers';
|
||||
|
||||
const SETTLEMENT_ENTRY_STATUSES = new Set(['CLOSED', 'PENDING_SETTLEMENT', 'SETTLED']);
|
||||
const STAT_MARKET_REQUIREMENTS = {
|
||||
FT_CORNERS_HANDICAP: ['homeCorners', 'awayCorners'],
|
||||
FT_CORNERS_OVER_UNDER: ['homeCorners', 'awayCorners'],
|
||||
FT_CARDS_OVER_UNDER: ['homeCards', 'awayCards'],
|
||||
} as const satisfies Record<string, readonly (keyof MatchStatsInput)[]>;
|
||||
|
||||
type TxClient = Prisma.TransactionClient;
|
||||
type PrismaClientLike = PrismaService | TxClient;
|
||||
type SettlementScoreSource = ScoreInput & MatchStatsInput & { winnerTeamId?: bigint | null };
|
||||
|
||||
type BetSelectionLeg = {
|
||||
id: bigint;
|
||||
@@ -36,13 +47,17 @@ type BetSelectionLeg = {
|
||||
export class SettlementService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private wallet: WalletService,
|
||||
private funds: FundsPostingService,
|
||||
private agents: AgentsService,
|
||||
) {}
|
||||
|
||||
private async resolveWinnerTeamCode(winnerTeamId: bigint | null | undefined): Promise<string | null> {
|
||||
private async resolveWinnerTeamCode(
|
||||
winnerTeamId: bigint | null | undefined,
|
||||
tx?: TxClient,
|
||||
): Promise<string | null> {
|
||||
if (!winnerTeamId) return null;
|
||||
const team = await this.prisma.team.findUnique({ where: { id: winnerTeamId } });
|
||||
const client: PrismaClientLike = tx ?? this.prisma;
|
||||
const team = await client.team.findUnique({ where: { id: winnerTeamId } });
|
||||
return team?.code ?? null;
|
||||
}
|
||||
|
||||
@@ -68,6 +83,7 @@ export class SettlementService {
|
||||
sel: BetSelectionLeg,
|
||||
selectionCode: string,
|
||||
scoreInput: ScoreInput,
|
||||
statsInput: MatchStatsInput,
|
||||
winnerTeamCode: string | null,
|
||||
) {
|
||||
return {
|
||||
@@ -76,6 +92,7 @@ export class SettlementService {
|
||||
handicapLine: sel.handicapLine != null ? Number(sel.handicapLine) : null,
|
||||
totalLine: sel.totalLine != null ? Number(sel.totalLine) : null,
|
||||
score: scoreInput,
|
||||
stats: statsInput,
|
||||
templateScores: templateScoresForMarket(sel.marketType),
|
||||
winnerTeamCode,
|
||||
};
|
||||
@@ -85,9 +102,71 @@ export class SettlementService {
|
||||
sel: BetSelectionLeg,
|
||||
selectionCode: string,
|
||||
scoreInput: ScoreInput,
|
||||
statsInput: MatchStatsInput,
|
||||
winnerTeamCode: string | null,
|
||||
): SelectionResult {
|
||||
return settleSelection(this.buildSettleInput(sel, selectionCode, scoreInput, winnerTeamCode));
|
||||
return settleSelection(this.buildSettleInput(sel, selectionCode, scoreInput, statsInput, winnerTeamCode));
|
||||
}
|
||||
|
||||
private cardTotalFromBreakdown(
|
||||
total: number | null | undefined,
|
||||
yellow: number | null | undefined,
|
||||
red: number | null | undefined,
|
||||
): number | null {
|
||||
const hasBreakdown = yellow != null || red != null;
|
||||
if (!hasBreakdown) return total ?? null;
|
||||
if (yellow == null || red == null) return null;
|
||||
return yellow + red;
|
||||
}
|
||||
|
||||
private statsInputFromSource(source: MatchStatsInput): MatchStatsInput {
|
||||
const homeYellowCards = source.homeYellowCards ?? null;
|
||||
const awayYellowCards = source.awayYellowCards ?? null;
|
||||
const homeRedCards = source.homeRedCards ?? null;
|
||||
const awayRedCards = source.awayRedCards ?? null;
|
||||
return {
|
||||
homeCorners: source.homeCorners ?? null,
|
||||
awayCorners: source.awayCorners ?? null,
|
||||
homeYellowCards,
|
||||
awayYellowCards,
|
||||
homeRedCards,
|
||||
awayRedCards,
|
||||
homeCards: this.cardTotalFromBreakdown(source.homeCards, homeYellowCards, homeRedCards),
|
||||
awayCards: this.cardTotalFromBreakdown(source.awayCards, awayYellowCards, awayRedCards),
|
||||
};
|
||||
}
|
||||
|
||||
private collectRequiredStatFields(marketTypes: Iterable<string>) {
|
||||
const fields = new Set<keyof MatchStatsInput>();
|
||||
for (const marketType of marketTypes) {
|
||||
const required = STAT_MARKET_REQUIREMENTS[marketType as keyof typeof STAT_MARKET_REQUIREMENTS];
|
||||
if (!required) continue;
|
||||
for (const field of required) fields.add(field);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private async assertRequiredStatsAvailable(
|
||||
matchId: bigint,
|
||||
statsInput: MatchStatsInput,
|
||||
betMarketTypes: Iterable<string>,
|
||||
client?: PrismaClientLike,
|
||||
) {
|
||||
const db = client ?? this.prisma;
|
||||
const visibleMarkets = await db.market.findMany({
|
||||
where: { matchId, showOnPlayer: true },
|
||||
select: { marketType: true },
|
||||
});
|
||||
const required = this.collectRequiredStatFields([
|
||||
...visibleMarkets.map((market) => market.marketType),
|
||||
...betMarketTypes,
|
||||
]);
|
||||
const missing = [...required].filter((field) => statsInput[field] == null);
|
||||
if (missing.length) {
|
||||
throw appBadRequest('SETTLEMENT_FACTS_REQUIRED', {
|
||||
fields: missing.join(','),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private walletResultFromSelection(
|
||||
@@ -118,6 +197,7 @@ export class SettlementService {
|
||||
ftAway: number,
|
||||
operatorId: bigint,
|
||||
winnerTeamId?: bigint,
|
||||
statsInput: MatchStatsInput = {},
|
||||
) {
|
||||
const match = await this.prisma.match.findFirst({
|
||||
where: { id: matchId, deletedAt: null },
|
||||
@@ -146,6 +226,7 @@ export class SettlementService {
|
||||
}
|
||||
}
|
||||
|
||||
const stats = this.statsInputFromSource(statsInput);
|
||||
await this.prisma.matchScore.upsert({
|
||||
where: { matchId },
|
||||
create: {
|
||||
@@ -154,6 +235,14 @@ export class SettlementService {
|
||||
htAwayScore: match.isOutright ? 0 : htAway,
|
||||
ftHomeScore: match.isOutright ? 0 : ftHome,
|
||||
ftAwayScore: match.isOutright ? 0 : ftAway,
|
||||
homeCorners: match.isOutright ? null : stats.homeCorners,
|
||||
awayCorners: match.isOutright ? null : stats.awayCorners,
|
||||
homeYellowCards: match.isOutright ? null : stats.homeYellowCards,
|
||||
awayYellowCards: match.isOutright ? null : stats.awayYellowCards,
|
||||
homeRedCards: match.isOutright ? null : stats.homeRedCards,
|
||||
awayRedCards: match.isOutright ? null : stats.awayRedCards,
|
||||
homeCards: match.isOutright ? null : stats.homeCards,
|
||||
awayCards: match.isOutright ? null : stats.awayCards,
|
||||
winnerTeamId: match.isOutright ? winnerTeamId : null,
|
||||
recordedBy: operatorId,
|
||||
},
|
||||
@@ -162,6 +251,14 @@ export class SettlementService {
|
||||
htAwayScore: match.isOutright ? 0 : htAway,
|
||||
ftHomeScore: match.isOutright ? 0 : ftHome,
|
||||
ftAwayScore: match.isOutright ? 0 : ftAway,
|
||||
homeCorners: match.isOutright ? null : stats.homeCorners,
|
||||
awayCorners: match.isOutright ? null : stats.awayCorners,
|
||||
homeYellowCards: match.isOutright ? null : stats.homeYellowCards,
|
||||
awayYellowCards: match.isOutright ? null : stats.awayYellowCards,
|
||||
homeRedCards: match.isOutright ? null : stats.homeRedCards,
|
||||
awayRedCards: match.isOutright ? null : stats.awayRedCards,
|
||||
homeCards: match.isOutright ? null : stats.homeCards,
|
||||
awayCards: match.isOutright ? null : stats.awayCards,
|
||||
winnerTeamId: match.isOutright ? winnerTeamId : null,
|
||||
recordedBy: operatorId,
|
||||
},
|
||||
@@ -172,7 +269,15 @@ export class SettlementService {
|
||||
data: { status: 'PENDING_SETTLEMENT' },
|
||||
});
|
||||
|
||||
return { matchId, htHome, htAway, ftHome, ftAway, winnerTeamId: winnerTeamId?.toString() ?? null };
|
||||
return {
|
||||
matchId,
|
||||
htHome,
|
||||
htAway,
|
||||
ftHome,
|
||||
ftAway,
|
||||
...stats,
|
||||
winnerTeamId: winnerTeamId?.toString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async previewSettlement(
|
||||
@@ -185,6 +290,14 @@ export class SettlementService {
|
||||
htAway?: number;
|
||||
ftHome?: number;
|
||||
ftAway?: number;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
winnerTeamId?: bigint;
|
||||
},
|
||||
) {
|
||||
@@ -208,6 +321,14 @@ export class SettlementService {
|
||||
htAwayScore: scoreSource.htAway,
|
||||
ftHomeScore: scoreSource.ftHome,
|
||||
ftAwayScore: scoreSource.ftAway,
|
||||
homeCorners: scoreSource.homeCorners ?? null,
|
||||
awayCorners: scoreSource.awayCorners ?? null,
|
||||
homeYellowCards: scoreSource.homeYellowCards ?? null,
|
||||
awayYellowCards: scoreSource.awayYellowCards ?? null,
|
||||
homeRedCards: scoreSource.homeRedCards ?? null,
|
||||
awayRedCards: scoreSource.awayRedCards ?? null,
|
||||
homeCards: scoreSource.homeCards ?? null,
|
||||
awayCards: scoreSource.awayCards ?? null,
|
||||
status: 'PREVIEW',
|
||||
totalBets: computation.pendingBets.length,
|
||||
totalPayout: computation.totalPayout,
|
||||
@@ -248,6 +369,14 @@ export class SettlementService {
|
||||
htAway: batch.htAwayScore ?? 0,
|
||||
ftHome: batch.ftHomeScore ?? 0,
|
||||
ftAway: batch.ftAwayScore ?? 0,
|
||||
homeCorners: batch.homeCorners,
|
||||
awayCorners: batch.awayCorners,
|
||||
homeYellowCards: batch.homeYellowCards,
|
||||
awayYellowCards: batch.awayYellowCards,
|
||||
homeRedCards: batch.homeRedCards,
|
||||
awayRedCards: batch.awayRedCards,
|
||||
homeCards: batch.homeCards,
|
||||
awayCards: batch.awayCards,
|
||||
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||
});
|
||||
const itemsPage = this.paginatePreviewItems(computation.items, opts);
|
||||
@@ -343,12 +472,21 @@ export class SettlementService {
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
winnerTeamId?: bigint | null;
|
||||
},
|
||||
operatorId: bigint,
|
||||
tx?: Parameters<Parameters<PrismaService['$transaction']>[0]>[0],
|
||||
) {
|
||||
const client = tx ?? this.prisma;
|
||||
const stats = this.statsInputFromSource(scoreSource);
|
||||
await client.matchScore.upsert({
|
||||
where: { matchId },
|
||||
create: {
|
||||
@@ -357,6 +495,14 @@ export class SettlementService {
|
||||
htAwayScore: scoreSource.htAway,
|
||||
ftHomeScore: scoreSource.ftHome,
|
||||
ftAwayScore: scoreSource.ftAway,
|
||||
homeCorners: stats.homeCorners,
|
||||
awayCorners: stats.awayCorners,
|
||||
homeYellowCards: stats.homeYellowCards,
|
||||
awayYellowCards: stats.awayYellowCards,
|
||||
homeRedCards: stats.homeRedCards,
|
||||
awayRedCards: stats.awayRedCards,
|
||||
homeCards: stats.homeCards,
|
||||
awayCards: stats.awayCards,
|
||||
winnerTeamId: scoreSource.winnerTeamId ?? null,
|
||||
recordedBy: operatorId,
|
||||
},
|
||||
@@ -365,6 +511,14 @@ export class SettlementService {
|
||||
htAwayScore: scoreSource.htAway,
|
||||
ftHomeScore: scoreSource.ftHome,
|
||||
ftAwayScore: scoreSource.ftAway,
|
||||
homeCorners: stats.homeCorners,
|
||||
awayCorners: stats.awayCorners,
|
||||
homeYellowCards: stats.homeYellowCards,
|
||||
awayYellowCards: stats.awayYellowCards,
|
||||
homeRedCards: stats.homeRedCards,
|
||||
awayRedCards: stats.awayRedCards,
|
||||
homeCards: stats.homeCards,
|
||||
awayCards: stats.awayCards,
|
||||
winnerTeamId: scoreSource.winnerTeamId ?? null,
|
||||
recordedBy: operatorId,
|
||||
},
|
||||
@@ -379,6 +533,14 @@ export class SettlementService {
|
||||
htAway?: number;
|
||||
ftHome?: number;
|
||||
ftAway?: number;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
winnerTeamId?: bigint;
|
||||
},
|
||||
) {
|
||||
@@ -387,6 +549,14 @@ export class SettlementService {
|
||||
opts?.htAway !== undefined ||
|
||||
opts?.ftHome !== undefined ||
|
||||
opts?.ftAway !== undefined ||
|
||||
opts?.homeCorners !== undefined ||
|
||||
opts?.awayCorners !== undefined ||
|
||||
opts?.homeYellowCards !== undefined ||
|
||||
opts?.awayYellowCards !== undefined ||
|
||||
opts?.homeRedCards !== undefined ||
|
||||
opts?.awayRedCards !== undefined ||
|
||||
opts?.homeCards !== undefined ||
|
||||
opts?.awayCards !== undefined ||
|
||||
opts?.winnerTeamId !== undefined;
|
||||
|
||||
if (hasRequestScore) {
|
||||
@@ -402,11 +572,22 @@ export class SettlementService {
|
||||
});
|
||||
if (!outrightSel) throw appBadRequest('SETTLEMENT_WINNER_NOT_IN_MARKET');
|
||||
}
|
||||
const stats = this.statsInputFromSource({
|
||||
homeCorners: opts?.homeCorners ?? null,
|
||||
awayCorners: opts?.awayCorners ?? null,
|
||||
homeYellowCards: opts?.homeYellowCards ?? null,
|
||||
awayYellowCards: opts?.awayYellowCards ?? null,
|
||||
homeRedCards: opts?.homeRedCards ?? null,
|
||||
awayRedCards: opts?.awayRedCards ?? null,
|
||||
homeCards: opts?.homeCards ?? null,
|
||||
awayCards: opts?.awayCards ?? null,
|
||||
});
|
||||
return {
|
||||
htHome: opts?.htHome ?? 0,
|
||||
htAway: opts?.htAway ?? 0,
|
||||
ftHome: opts?.ftHome ?? 0,
|
||||
ftAway: opts?.ftAway ?? 0,
|
||||
...stats,
|
||||
winnerTeamId: isOutright ? (opts?.winnerTeamId ?? null) : null,
|
||||
};
|
||||
}
|
||||
@@ -419,21 +600,24 @@ export class SettlementService {
|
||||
htAway: score.htAwayScore ?? 0,
|
||||
ftHome: score.ftHomeScore ?? 0,
|
||||
ftAway: score.ftAwayScore ?? 0,
|
||||
homeCorners: score.homeCorners,
|
||||
awayCorners: score.awayCorners,
|
||||
homeYellowCards: score.homeYellowCards,
|
||||
awayYellowCards: score.awayYellowCards,
|
||||
homeRedCards: score.homeRedCards,
|
||||
awayRedCards: score.awayRedCards,
|
||||
homeCards: score.homeCards,
|
||||
awayCards: score.awayCards,
|
||||
winnerTeamId: score.winnerTeamId,
|
||||
};
|
||||
}
|
||||
|
||||
private async computePreviewComputation(
|
||||
matchId: bigint,
|
||||
scoreSource?: {
|
||||
htHome: number;
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
winnerTeamId?: bigint | null;
|
||||
},
|
||||
scoreSource?: SettlementScoreSource,
|
||||
) {
|
||||
let scoreInput: ScoreInput;
|
||||
let statsInput: MatchStatsInput;
|
||||
let winnerTeamCode: string | null;
|
||||
|
||||
if (scoreSource) {
|
||||
@@ -443,6 +627,7 @@ export class SettlementService {
|
||||
ftHome: scoreSource.ftHome,
|
||||
ftAway: scoreSource.ftAway,
|
||||
};
|
||||
statsInput = this.statsInputFromSource(scoreSource);
|
||||
winnerTeamCode = await this.resolveWinnerTeamCode(scoreSource.winnerTeamId ?? null);
|
||||
} else {
|
||||
const score = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
||||
@@ -453,6 +638,7 @@ export class SettlementService {
|
||||
ftHome: score.ftHomeScore ?? 0,
|
||||
ftAway: score.ftAwayScore ?? 0,
|
||||
};
|
||||
statsInput = this.statsInputFromSource(score);
|
||||
winnerTeamCode = await this.resolveWinnerTeamCode(score.winnerTeamId);
|
||||
}
|
||||
|
||||
@@ -465,6 +651,11 @@ export class SettlementService {
|
||||
});
|
||||
|
||||
const selectionCodes = await this.loadSelectionCodes(pendingBets);
|
||||
await this.assertRequiredStatsAvailable(
|
||||
matchId,
|
||||
statsInput,
|
||||
pendingBets.flatMap((bet) => bet.selections.map((sel) => sel.marketType)),
|
||||
);
|
||||
|
||||
let totalPayout = new Decimal(0);
|
||||
let totalRefund = new Decimal(0);
|
||||
@@ -487,7 +678,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
const payout = calculatePayout(bet.stake, sel.odds, result);
|
||||
if (result === 'WIN' || result === 'HALF_WIN') wonLegsOnMatch += 1;
|
||||
items.push({ betId: bet.id, betNo: bet.betNo, betType: 'SINGLE', result, payout });
|
||||
@@ -504,7 +695,7 @@ export class SettlementService {
|
||||
);
|
||||
return {
|
||||
odds: sel.odds,
|
||||
result: this.settleLegResult(sel, code, scoreInput, winnerTeamCode),
|
||||
result: this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode),
|
||||
};
|
||||
});
|
||||
const parlay = calculateParlayPayout(bet.stake, legResults);
|
||||
@@ -528,7 +719,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const legResult = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const legResult = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
if (legResult === 'WIN' || legResult === 'HALF_WIN') wonLegsOnMatch += 1;
|
||||
}
|
||||
|
||||
@@ -536,6 +727,7 @@ export class SettlementService {
|
||||
bet,
|
||||
matchId,
|
||||
scoreInput,
|
||||
statsInput,
|
||||
winnerTeamCode,
|
||||
selectionCodes,
|
||||
);
|
||||
@@ -552,16 +744,6 @@ export class SettlementService {
|
||||
} else if (preview.betResult === 'WON') {
|
||||
totalPayout = totalPayout.add(preview.payout);
|
||||
}
|
||||
} else if (preview.kind === 'LOST_ON_THIS_MATCH') {
|
||||
lostOnThisMatch += 1;
|
||||
items.push({
|
||||
betId: bet.id,
|
||||
betNo: bet.betNo,
|
||||
betType: 'PARLAY',
|
||||
result: 'LOST',
|
||||
payout: preview.payout,
|
||||
note: '本场已有输腿,串关整单作废',
|
||||
});
|
||||
} else {
|
||||
pendingOtherMatches += 1;
|
||||
items.push({
|
||||
@@ -570,7 +752,7 @@ export class SettlementService {
|
||||
betType: 'PARLAY',
|
||||
result: 'PENDING_OTHER_MATCHES',
|
||||
payout: new Decimal(0),
|
||||
note: '本场腿已出结果,待其他场次结算后派彩',
|
||||
note: '本场腿已出结果,待其他场次结算后统一结算',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -593,24 +775,12 @@ export class SettlementService {
|
||||
bet: { stake: Decimal; selections: BetSelectionLeg[] },
|
||||
matchId: bigint,
|
||||
scoreInput: ScoreInput,
|
||||
statsInput: MatchStatsInput,
|
||||
winnerTeamCode: string | null,
|
||||
selectionCodes: Map<string, string | null>,
|
||||
):
|
||||
| { kind: 'SETTLED'; betResult: 'WON' | 'LOST' | 'PUSH'; payout: Decimal }
|
||||
| { kind: 'LOST_ON_THIS_MATCH'; payout: Decimal }
|
||||
| { kind: 'PENDING_OTHER_MATCHES' } {
|
||||
for (const sel of bet.selections) {
|
||||
if (sel.matchId?.toString() !== matchId.toString()) continue;
|
||||
const code = resolveSelectionCode(
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
if (result === 'LOSE' || result === 'HALF_LOSE') {
|
||||
return { kind: 'LOST_ON_THIS_MATCH', payout: new Decimal(0) };
|
||||
}
|
||||
}
|
||||
|
||||
const legResults: Array<{ odds: Decimal; result: SelectionResult }> = [];
|
||||
for (const sel of bet.selections) {
|
||||
if (sel.matchId?.toString() === matchId.toString()) {
|
||||
@@ -620,7 +790,7 @@ export class SettlementService {
|
||||
);
|
||||
legResults.push({
|
||||
odds: sel.odds,
|
||||
result: this.settleLegResult(sel, code, scoreInput, winnerTeamCode),
|
||||
result: this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode),
|
||||
});
|
||||
} else if (sel.resultStatus) {
|
||||
legResults.push({
|
||||
@@ -641,6 +811,7 @@ export class SettlementService {
|
||||
|
||||
private async loadSelectionCodes(
|
||||
bets: Array<{ selections: Array<{ selectionId: bigint }> }>,
|
||||
tx?: TxClient,
|
||||
): Promise<Map<string, string | null>> {
|
||||
const ids = new Set<bigint>();
|
||||
for (const bet of bets) {
|
||||
@@ -648,7 +819,8 @@ export class SettlementService {
|
||||
}
|
||||
if (!ids.size) return new Map();
|
||||
|
||||
const rows = await this.prisma.marketSelection.findMany({
|
||||
const client: PrismaClientLike = tx ?? this.prisma;
|
||||
const rows = await client.marketSelection.findMany({
|
||||
where: { id: { in: [...ids] } },
|
||||
select: { id: true, selectionCode: true },
|
||||
});
|
||||
@@ -670,36 +842,63 @@ export class SettlementService {
|
||||
await this.assertOutrightLeagueFixturesSettled(batch.match);
|
||||
}
|
||||
|
||||
const scoreInput: ScoreInput = {
|
||||
htHome: batch.htHomeScore ?? 0,
|
||||
htAway: batch.htAwayScore ?? 0,
|
||||
ftHome: batch.ftHomeScore ?? 0,
|
||||
ftAway: batch.ftAwayScore ?? 0,
|
||||
};
|
||||
const existingScore = await this.prisma.matchScore.findUnique({
|
||||
where: { matchId: batch.matchId },
|
||||
});
|
||||
const winnerTeamCode = await this.resolveWinnerTeamCode(existingScore?.winnerTeamId ?? null);
|
||||
|
||||
const pendingBets = await this.prisma.bet.findMany({
|
||||
where: {
|
||||
status: 'PENDING',
|
||||
selections: { some: { matchId: batch.matchId } },
|
||||
},
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } }, user: true },
|
||||
});
|
||||
|
||||
const selectionCodes = await this.loadSelectionCodes(pendingBets);
|
||||
const agentIds = new Set<bigint>();
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const claimed = await tx.settlementBatch.updateMany({
|
||||
where: { id: batchId, status: 'PREVIEW' },
|
||||
data: { status: 'CONFIRMING', operatorId },
|
||||
});
|
||||
if (claimed.count !== 1) throw appBadRequest('SETTLEMENT_BATCH_ALREADY_CONFIRMED');
|
||||
|
||||
const currentBatch = await tx.settlementBatch.findUnique({
|
||||
where: { id: batchId },
|
||||
include: { match: true },
|
||||
});
|
||||
if (!currentBatch) throw appNotFound('SETTLEMENT_BATCH_NOT_FOUND');
|
||||
if (currentBatch.match.status !== 'PENDING_SETTLEMENT') {
|
||||
throw appBadRequest('MATCH_NOT_SETTLEABLE');
|
||||
}
|
||||
|
||||
const scoreInput: ScoreInput = {
|
||||
htHome: currentBatch.htHomeScore ?? 0,
|
||||
htAway: currentBatch.htAwayScore ?? 0,
|
||||
ftHome: currentBatch.ftHomeScore ?? 0,
|
||||
ftAway: currentBatch.ftAwayScore ?? 0,
|
||||
};
|
||||
const statsInput = this.statsInputFromSource(currentBatch);
|
||||
const existingScore = await tx.matchScore.findUnique({
|
||||
where: { matchId: currentBatch.matchId },
|
||||
});
|
||||
const winnerTeamCode = await this.resolveWinnerTeamCode(
|
||||
existingScore?.winnerTeamId ?? null,
|
||||
tx,
|
||||
);
|
||||
|
||||
const pendingBets = await tx.bet.findMany({
|
||||
where: {
|
||||
status: 'PENDING',
|
||||
selections: { some: { matchId: currentBatch.matchId } },
|
||||
},
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } }, user: true },
|
||||
});
|
||||
const selectionCodes = await this.loadSelectionCodes(pendingBets, tx);
|
||||
await this.assertRequiredStatsAvailable(
|
||||
currentBatch.matchId,
|
||||
statsInput,
|
||||
pendingBets.flatMap((bet) => bet.selections.map((sel) => sel.marketType)),
|
||||
tx,
|
||||
);
|
||||
let settledCount = 0;
|
||||
|
||||
await this.upsertMatchScoreRecord(
|
||||
batch.matchId,
|
||||
currentBatch.matchId,
|
||||
{
|
||||
htHome: scoreInput.htHome,
|
||||
htAway: scoreInput.htAway,
|
||||
ftHome: scoreInput.ftHome,
|
||||
ftAway: scoreInput.ftAway,
|
||||
...statsInput,
|
||||
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||
},
|
||||
operatorId,
|
||||
@@ -713,34 +912,37 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
const payout = calculatePayout(bet.stake, sel.odds, result);
|
||||
const betStatus = this.betStatusFromSelection(result);
|
||||
|
||||
await tx.bet.update({
|
||||
where: { id: bet.id },
|
||||
const updatedBet = await tx.bet.updateMany({
|
||||
where: { id: bet.id, status: 'PENDING' },
|
||||
data: {
|
||||
status: betStatus,
|
||||
actualReturn: payout,
|
||||
settledAt: new Date(),
|
||||
},
|
||||
});
|
||||
if (updatedBet.count !== 1) continue;
|
||||
|
||||
await tx.betSelection.update({
|
||||
where: { id: sel.id },
|
||||
data: { resultStatus: result, effectiveOdds: sel.odds },
|
||||
});
|
||||
|
||||
await this.wallet.settleBet(
|
||||
bet.userId,
|
||||
bet.stake,
|
||||
await this.funds.settleBet({
|
||||
userId: bet.userId,
|
||||
stake: bet.stake,
|
||||
payout,
|
||||
bet.betNo,
|
||||
this.walletResultFromSelection(result),
|
||||
betNo: bet.betNo,
|
||||
batchNo: batch.batchNo,
|
||||
result: this.walletResultFromSelection(result),
|
||||
tx,
|
||||
);
|
||||
});
|
||||
|
||||
if (bet.agentId) agentIds.add(bet.agentId);
|
||||
settledCount += 1;
|
||||
|
||||
await tx.settlementItem.create({
|
||||
data: {
|
||||
@@ -758,7 +960,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
legResults.push({ odds: sel.odds, result });
|
||||
await tx.betSelection.update({
|
||||
where: { id: sel.id },
|
||||
@@ -773,29 +975,33 @@ export class SettlementService {
|
||||
? 'PUSH'
|
||||
: 'WON';
|
||||
|
||||
await tx.bet.update({
|
||||
where: { id: bet.id },
|
||||
const updatedBet = await tx.bet.updateMany({
|
||||
where: { id: bet.id, status: 'PENDING' },
|
||||
data: {
|
||||
status: betStatus,
|
||||
actualReturn: parlayResult.payout,
|
||||
settledAt: new Date(),
|
||||
},
|
||||
});
|
||||
if (updatedBet.count !== 1) continue;
|
||||
|
||||
await this.wallet.settleBet(
|
||||
bet.userId,
|
||||
bet.stake,
|
||||
parlayResult.payout,
|
||||
bet.betNo,
|
||||
parlayResult.betResult === 'LOST'
|
||||
? 'LOSE'
|
||||
: parlayResult.betResult === 'PUSH'
|
||||
? 'PUSH'
|
||||
: 'WIN',
|
||||
await this.funds.settleBet({
|
||||
userId: bet.userId,
|
||||
stake: bet.stake,
|
||||
payout: parlayResult.payout,
|
||||
betNo: bet.betNo,
|
||||
batchNo: batch.batchNo,
|
||||
result:
|
||||
parlayResult.betResult === 'LOST'
|
||||
? 'LOSE'
|
||||
: parlayResult.betResult === 'PUSH'
|
||||
? 'PUSH'
|
||||
: 'WIN',
|
||||
tx,
|
||||
);
|
||||
});
|
||||
|
||||
if (bet.agentId) agentIds.add(bet.agentId);
|
||||
settledCount += 1;
|
||||
|
||||
await tx.settlementItem.create({
|
||||
data: {
|
||||
@@ -813,7 +1019,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
await tx.betSelection.update({
|
||||
where: { id: sel.id },
|
||||
data: { resultStatus: result },
|
||||
@@ -840,29 +1046,33 @@ export class SettlementService {
|
||||
? 'PUSH'
|
||||
: 'WON';
|
||||
|
||||
await tx.bet.update({
|
||||
where: { id: bet.id },
|
||||
const updatedBet = await tx.bet.updateMany({
|
||||
where: { id: bet.id, status: 'PENDING' },
|
||||
data: {
|
||||
status: betStatus,
|
||||
actualReturn: parlayResult.payout,
|
||||
settledAt: new Date(),
|
||||
},
|
||||
});
|
||||
if (updatedBet.count !== 1) continue;
|
||||
|
||||
await this.wallet.settleBet(
|
||||
bet.userId,
|
||||
bet.stake,
|
||||
parlayResult.payout,
|
||||
bet.betNo,
|
||||
parlayResult.betResult === 'LOST'
|
||||
? 'LOSE'
|
||||
: parlayResult.betResult === 'PUSH'
|
||||
? 'PUSH'
|
||||
: 'WIN',
|
||||
await this.funds.settleBet({
|
||||
userId: bet.userId,
|
||||
stake: bet.stake,
|
||||
payout: parlayResult.payout,
|
||||
betNo: bet.betNo,
|
||||
batchNo: batch.batchNo,
|
||||
result:
|
||||
parlayResult.betResult === 'LOST'
|
||||
? 'LOSE'
|
||||
: parlayResult.betResult === 'PUSH'
|
||||
? 'PUSH'
|
||||
: 'WIN',
|
||||
tx,
|
||||
);
|
||||
});
|
||||
|
||||
if (bet.agentId) agentIds.add(bet.agentId);
|
||||
settledCount += 1;
|
||||
|
||||
await tx.settlementItem.create({
|
||||
data: {
|
||||
@@ -879,11 +1089,16 @@ export class SettlementService {
|
||||
|
||||
await tx.settlementBatch.update({
|
||||
where: { id: batchId },
|
||||
data: { status: 'CONFIRMED', confirmedAt: new Date() },
|
||||
data: {
|
||||
status: 'CONFIRMED',
|
||||
confirmedAt: new Date(),
|
||||
operatorId,
|
||||
totalBets: settledCount,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.match.update({
|
||||
where: { id: batch.matchId },
|
||||
where: { id: currentBatch.matchId },
|
||||
data: { status: 'SETTLED' },
|
||||
});
|
||||
});
|
||||
@@ -1090,6 +1305,7 @@ export class SettlementService {
|
||||
},
|
||||
matchId: bigint,
|
||||
scoreInput: ScoreInput,
|
||||
statsInput: MatchStatsInput,
|
||||
winnerTeamCode: string | null,
|
||||
selectionCodes: Map<string, string | null>,
|
||||
) {
|
||||
@@ -1099,7 +1315,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
const result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
const payout = calculatePayout(bet.stake, sel.odds, result);
|
||||
return {
|
||||
payout,
|
||||
@@ -1118,7 +1334,7 @@ export class SettlementService {
|
||||
selectionCodes.get(sel.selectionId.toString()),
|
||||
sel.selectionNameSnapshot,
|
||||
);
|
||||
result = this.settleLegResult(sel, code, scoreInput, winnerTeamCode);
|
||||
result = this.settleLegResult(sel, code, scoreInput, statsInput, winnerTeamCode);
|
||||
legUpdates.set(sel.id.toString(), result);
|
||||
} else {
|
||||
if (!sel.resultStatus) {
|
||||
@@ -1142,7 +1358,7 @@ export class SettlementService {
|
||||
|
||||
async previewResettlement(
|
||||
matchId: bigint,
|
||||
scoreInput: ScoreInput,
|
||||
scoreInput: ScoreInput & MatchStatsInput,
|
||||
operatorId: bigint,
|
||||
reason?: string,
|
||||
winnerTeamId?: bigint,
|
||||
@@ -1158,6 +1374,7 @@ export class SettlementService {
|
||||
const winnerTeamCode = winnerTeamId
|
||||
? await this.resolveWinnerTeamCode(winnerTeamId)
|
||||
: null;
|
||||
const statsInput = this.statsInputFromSource(scoreInput);
|
||||
|
||||
const settledBets = await this.prisma.bet.findMany({
|
||||
where: {
|
||||
@@ -1168,6 +1385,11 @@ export class SettlementService {
|
||||
});
|
||||
|
||||
const selectionCodes = await this.loadSelectionCodes(settledBets);
|
||||
await this.assertRequiredStatsAvailable(
|
||||
matchId,
|
||||
statsInput,
|
||||
settledBets.flatMap((bet) => bet.selections.map((sel) => sel.marketType)),
|
||||
);
|
||||
const items: Array<{
|
||||
betId: bigint;
|
||||
betNo: string;
|
||||
@@ -1187,6 +1409,7 @@ export class SettlementService {
|
||||
bet,
|
||||
matchId,
|
||||
scoreInput,
|
||||
statsInput,
|
||||
winnerTeamCode,
|
||||
selectionCodes,
|
||||
);
|
||||
@@ -1215,6 +1438,14 @@ export class SettlementService {
|
||||
htAwayScore: scoreInput.htAway,
|
||||
ftHomeScore: scoreInput.ftHome,
|
||||
ftAwayScore: scoreInput.ftAway,
|
||||
homeCorners: statsInput.homeCorners ?? null,
|
||||
awayCorners: statsInput.awayCorners ?? null,
|
||||
homeYellowCards: statsInput.homeYellowCards ?? null,
|
||||
awayYellowCards: statsInput.awayYellowCards ?? null,
|
||||
homeRedCards: statsInput.homeRedCards ?? null,
|
||||
awayRedCards: statsInput.awayRedCards ?? null,
|
||||
homeCards: statsInput.homeCards ?? null,
|
||||
awayCards: statsInput.awayCards ?? null,
|
||||
status: 'PREVIEW',
|
||||
totalBets: items.length,
|
||||
totalPayout: totalTopup,
|
||||
@@ -1233,6 +1464,7 @@ export class SettlementService {
|
||||
htAway: scoreInput.htAway,
|
||||
ftHome: scoreInput.ftHome,
|
||||
ftAway: scoreInput.ftAway,
|
||||
...statsInput,
|
||||
winnerTeamId,
|
||||
},
|
||||
operatorId,
|
||||
@@ -1259,36 +1491,60 @@ export class SettlementService {
|
||||
if (!batch.isResettle) throw appBadRequest('RESETTLE_BATCH_ONLY');
|
||||
if (batch.status !== 'PREVIEW') throw appBadRequest('SETTLEMENT_BATCH_ALREADY_CONFIRMED');
|
||||
|
||||
const scoreInput: ScoreInput = {
|
||||
htHome: batch.htHomeScore ?? 0,
|
||||
htAway: batch.htAwayScore ?? 0,
|
||||
ftHome: batch.ftHomeScore ?? 0,
|
||||
ftAway: batch.ftAwayScore ?? 0,
|
||||
};
|
||||
const existingScore = await this.prisma.matchScore.findUnique({
|
||||
where: { matchId: batch.matchId },
|
||||
});
|
||||
const winnerTeamCode = await this.resolveWinnerTeamCode(existingScore?.winnerTeamId ?? null);
|
||||
|
||||
const settledBets = await this.prisma.bet.findMany({
|
||||
where: {
|
||||
status: { in: ['WON', 'LOST', 'PUSH', 'VOID'] },
|
||||
selections: { some: { matchId: batch.matchId } },
|
||||
},
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } } },
|
||||
});
|
||||
|
||||
const selectionCodes = await this.loadSelectionCodes(settledBets);
|
||||
const agentIds = new Set<bigint>();
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const claimed = await tx.settlementBatch.updateMany({
|
||||
where: { id: batchId, status: 'PREVIEW' },
|
||||
data: { status: 'CONFIRMING', operatorId },
|
||||
});
|
||||
if (claimed.count !== 1) throw appBadRequest('SETTLEMENT_BATCH_ALREADY_CONFIRMED');
|
||||
|
||||
const currentBatch = await tx.settlementBatch.findUnique({
|
||||
where: { id: batchId },
|
||||
include: { match: true },
|
||||
});
|
||||
if (!currentBatch) throw appNotFound('SETTLEMENT_BATCH_NOT_FOUND');
|
||||
|
||||
const scoreInput: ScoreInput = {
|
||||
htHome: currentBatch.htHomeScore ?? 0,
|
||||
htAway: currentBatch.htAwayScore ?? 0,
|
||||
ftHome: currentBatch.ftHomeScore ?? 0,
|
||||
ftAway: currentBatch.ftAwayScore ?? 0,
|
||||
};
|
||||
const statsInput = this.statsInputFromSource(currentBatch);
|
||||
const existingScore = await tx.matchScore.findUnique({
|
||||
where: { matchId: currentBatch.matchId },
|
||||
});
|
||||
const winnerTeamCode = await this.resolveWinnerTeamCode(
|
||||
existingScore?.winnerTeamId ?? null,
|
||||
tx,
|
||||
);
|
||||
|
||||
const settledBets = await tx.bet.findMany({
|
||||
where: {
|
||||
status: { in: ['WON', 'LOST', 'PUSH', 'VOID'] },
|
||||
selections: { some: { matchId: currentBatch.matchId } },
|
||||
},
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } } },
|
||||
});
|
||||
const selectionCodes = await this.loadSelectionCodes(settledBets, tx);
|
||||
await this.assertRequiredStatsAvailable(
|
||||
currentBatch.matchId,
|
||||
statsInput,
|
||||
settledBets.flatMap((bet) => bet.selections.map((sel) => sel.marketType)),
|
||||
tx,
|
||||
);
|
||||
let affectedCount = 0;
|
||||
|
||||
await this.upsertMatchScoreRecord(
|
||||
batch.matchId,
|
||||
currentBatch.matchId,
|
||||
{
|
||||
htHome: scoreInput.htHome,
|
||||
htAway: scoreInput.htAway,
|
||||
ftHome: scoreInput.ftHome,
|
||||
ftAway: scoreInput.ftAway,
|
||||
...statsInput,
|
||||
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||
},
|
||||
operatorId,
|
||||
@@ -1299,8 +1555,9 @@ export class SettlementService {
|
||||
const oldPayout = new Decimal(bet.actualReturn);
|
||||
const outcome = await this.computeBetOutcome(
|
||||
bet,
|
||||
batch.matchId,
|
||||
currentBatch.matchId,
|
||||
scoreInput,
|
||||
statsInput,
|
||||
winnerTeamCode,
|
||||
selectionCodes,
|
||||
);
|
||||
@@ -1328,9 +1585,16 @@ export class SettlementService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.wallet.applyResettleDelta(bet.userId, delta, bet.betNo, tx);
|
||||
await this.funds.applyResettleDelta({
|
||||
userId: bet.userId,
|
||||
delta,
|
||||
betNo: bet.betNo,
|
||||
batchNo: currentBatch.batchNo,
|
||||
tx,
|
||||
});
|
||||
|
||||
if (bet.agentId) agentIds.add(bet.agentId);
|
||||
affectedCount += 1;
|
||||
|
||||
await tx.settlementItem.create({
|
||||
data: {
|
||||
@@ -1345,7 +1609,12 @@ export class SettlementService {
|
||||
|
||||
await tx.settlementBatch.update({
|
||||
where: { id: batchId },
|
||||
data: { status: 'CONFIRMED', confirmedAt: new Date(), operatorId },
|
||||
data: {
|
||||
status: 'CONFIRMED',
|
||||
confirmedAt: new Date(),
|
||||
operatorId,
|
||||
totalBets: affectedCount,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1357,18 +1626,57 @@ export class SettlementService {
|
||||
}
|
||||
|
||||
async voidMatchBets(matchId: bigint) {
|
||||
const bets = await this.prisma.bet.findMany({
|
||||
where: { status: 'PENDING', selections: { some: { matchId } } },
|
||||
return this.voidMatchBetsInTransaction(matchId);
|
||||
}
|
||||
|
||||
async cancelMatchAndVoidBets(matchId: bigint) {
|
||||
return this.voidMatchBetsInTransaction(matchId, { cancelMatch: true });
|
||||
}
|
||||
|
||||
private async voidMatchBetsInTransaction(
|
||||
matchId: bigint,
|
||||
options: { cancelMatch?: boolean } = {},
|
||||
) {
|
||||
const agentIds = new Set<bigint>();
|
||||
|
||||
const voidedCount = await this.prisma.$transaction(async (tx) => {
|
||||
if (options.cancelMatch) {
|
||||
await tx.match.update({
|
||||
where: { id: matchId },
|
||||
data: { status: 'CANCELLED' },
|
||||
});
|
||||
}
|
||||
|
||||
const bets = await tx.bet.findMany({
|
||||
where: { status: 'PENDING', selections: { some: { matchId } } },
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
for (const bet of bets) {
|
||||
const updated = await tx.bet.updateMany({
|
||||
where: { id: bet.id, status: 'PENDING' },
|
||||
data: { status: 'VOID', actualReturn: bet.stake, settledAt: new Date() },
|
||||
});
|
||||
if (updated.count !== 1) continue;
|
||||
|
||||
await this.funds.voidBet({
|
||||
userId: bet.userId,
|
||||
stake: bet.stake,
|
||||
betNo: bet.betNo,
|
||||
businessKey: `void:${matchId}:${bet.betNo}`,
|
||||
tx,
|
||||
});
|
||||
if (bet.agentId) agentIds.add(bet.agentId);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
});
|
||||
|
||||
for (const bet of bets) {
|
||||
await this.wallet.settleBet(bet.userId, bet.stake, bet.stake, bet.betNo, 'VOID');
|
||||
await this.prisma.bet.update({
|
||||
where: { id: bet.id },
|
||||
data: { status: 'VOID', actualReturn: bet.stake, settledAt: new Date() },
|
||||
});
|
||||
for (const agentId of agentIds) {
|
||||
await this.agents.recalculateUsedCredit(agentId);
|
||||
}
|
||||
|
||||
return { voidedCount: bets.length };
|
||||
return { voidedCount };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user