fix(settlement): 要求封盘后才能结算并优化预览流程
封盘前禁止录入比分与生成预览;待结算未确认前可解除封盘。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -523,10 +523,11 @@ export const adminPagesMs: Record<string, string> = {
|
|||||||
'settlement.ht_score': 'Skor separuh masa',
|
'settlement.ht_score': 'Skor separuh masa',
|
||||||
'settlement.ft_score': 'Skor penuh masa',
|
'settlement.ft_score': 'Skor penuh masa',
|
||||||
'settlement.record_score': 'Simpan skor',
|
'settlement.record_score': 'Simpan skor',
|
||||||
'settlement.preview_hint': 'Isi skor dan klik pratonton — skor disimpan secara automatik',
|
'settlement.preview_hint': 'Pratonton menukar perlawanan ke menunggu penyelesaian (skor disimpan selepas pengesahan; boleh buka semula sebelum itu)',
|
||||||
'settlement.preview_btn': 'Pratonton penyelesaian',
|
'settlement.preview_btn': 'Pratonton penyelesaian',
|
||||||
'settlement.preview_failed': 'Gagal menjana pratonton penyelesaian',
|
'settlement.preview_failed': 'Gagal menjana pratonton penyelesaian',
|
||||||
'settlement.err_score_not_recorded': 'Sila masukkan skor separuh masa dan penuh masa sebelum penyelesaian',
|
'settlement.err_score_not_recorded': 'Sila masukkan skor separuh masa dan penuh masa sebelum penyelesaian',
|
||||||
|
'settlement.must_close_first': 'Tutup pertaruhan sebelum penyelesaian',
|
||||||
'settlement.preview_title': 'Pratonton penyelesaian',
|
'settlement.preview_title': 'Pratonton penyelesaian',
|
||||||
'settlement.single_count': 'Pertaruhan tunggal',
|
'settlement.single_count': 'Pertaruhan tunggal',
|
||||||
'settlement.est_payout': 'Anggaran bayaran',
|
'settlement.est_payout': 'Anggaran bayaran',
|
||||||
|
|||||||
@@ -546,10 +546,11 @@ export const adminPagesZh: Record<string, string> = {
|
|||||||
'settlement.ht_score': '半场比分',
|
'settlement.ht_score': '半场比分',
|
||||||
'settlement.ft_score': '全场比分',
|
'settlement.ft_score': '全场比分',
|
||||||
'settlement.record_score': '录入比分',
|
'settlement.record_score': '录入比分',
|
||||||
'settlement.preview_hint': '填写比分后点击生成预览,系统将自动保存并计算派彩',
|
'settlement.preview_hint': '填写比分后点击生成预览,赛事将进入待结算并计算派彩(正式比分在确认结算后保存;未确认前仍可解除封盘)',
|
||||||
'settlement.preview_btn': '生成结算预览',
|
'settlement.preview_btn': '生成结算预览',
|
||||||
'settlement.preview_failed': '生成结算预览失败',
|
'settlement.preview_failed': '生成结算预览失败',
|
||||||
'settlement.err_score_not_recorded': '请先填写半场与全场比分后再生成预览',
|
'settlement.err_score_not_recorded': '请先填写半场与全场比分后再生成预览',
|
||||||
|
'settlement.must_close_first': '请先封盘后再结算',
|
||||||
'settlement.preview_title': '结算预览',
|
'settlement.preview_title': '结算预览',
|
||||||
'settlement.single_count': '单关注单数',
|
'settlement.single_count': '单关注单数',
|
||||||
'settlement.preview_pending_bets': '待结算注单',
|
'settlement.preview_pending_bets': '待结算注单',
|
||||||
@@ -1516,10 +1517,11 @@ export const adminPagesEn: Record<string, string> = {
|
|||||||
'settlement.ht_score': 'Half-time score',
|
'settlement.ht_score': 'Half-time score',
|
||||||
'settlement.ft_score': 'Full-time score',
|
'settlement.ft_score': 'Full-time score',
|
||||||
'settlement.record_score': 'Save score',
|
'settlement.record_score': 'Save score',
|
||||||
'settlement.preview_hint': 'Enter scores and click preview — scores are saved automatically',
|
'settlement.preview_hint': 'Preview moves the match to pending settlement and calculates payouts (scores are saved on confirm; you can reopen betting before confirming)',
|
||||||
'settlement.preview_btn': 'Preview settlement',
|
'settlement.preview_btn': 'Preview settlement',
|
||||||
'settlement.preview_failed': 'Failed to generate settlement preview',
|
'settlement.preview_failed': 'Failed to generate settlement preview',
|
||||||
'settlement.err_score_not_recorded': 'Enter half-time and full-time scores before preview',
|
'settlement.err_score_not_recorded': 'Enter half-time and full-time scores before preview',
|
||||||
|
'settlement.must_close_first': 'Close betting before settlement',
|
||||||
'settlement.preview_title': 'Settlement preview',
|
'settlement.preview_title': 'Settlement preview',
|
||||||
'settlement.single_count': 'Single bets',
|
'settlement.single_count': 'Single bets',
|
||||||
'settlement.preview_pending_bets': 'Pending bets',
|
'settlement.preview_pending_bets': 'Pending bets',
|
||||||
|
|||||||
@@ -300,6 +300,15 @@ async function loadMatch() {
|
|||||||
router.replace('/matches');
|
router.replace('/matches');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const settleable =
|
||||||
|
detail.status === 'CLOSED' ||
|
||||||
|
detail.status === 'PENDING_SETTLEMENT' ||
|
||||||
|
detail.status === 'SETTLED';
|
||||||
|
if (!settleable) {
|
||||||
|
ElMessage.warning(t('settlement.must_close_first'));
|
||||||
|
router.replace('/matches');
|
||||||
|
return;
|
||||||
|
}
|
||||||
match.value = detail;
|
match.value = detail;
|
||||||
if (detail.score) {
|
if (detail.score) {
|
||||||
score.value = { ...detail.score };
|
score.value = { ...detail.score };
|
||||||
@@ -372,6 +381,7 @@ async function previewSettlement() {
|
|||||||
const itemsPage = data.data.items as PreviewItemsPage;
|
const itemsPage = data.data.items as PreviewItemsPage;
|
||||||
previewPage.value = itemsPage.page;
|
previewPage.value = itemsPage.page;
|
||||||
previewPageSize.value = itemsPage.pageSize;
|
previewPageSize.value = itemsPage.pageSize;
|
||||||
|
await loadMatch();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -166,10 +166,12 @@ function canCloseRow(row: unknown) {
|
|||||||
return matchStatus(row) === 'PUBLISHED';
|
return matchStatus(row) === 'PUBLISHED';
|
||||||
}
|
}
|
||||||
function canReopenRow(row: unknown) {
|
function canReopenRow(row: unknown) {
|
||||||
return matchStatus(row) === 'CLOSED';
|
const s = matchStatus(row);
|
||||||
|
return s === 'CLOSED' || s === 'PENDING_SETTLEMENT';
|
||||||
}
|
}
|
||||||
function canSettleRow(row: unknown) {
|
function canSettleRow(row: unknown) {
|
||||||
return matchStatus(row) !== 'DRAFT';
|
const s = matchStatus(row);
|
||||||
|
return s === 'CLOSED' || s === 'PENDING_SETTLEMENT' || s === 'SETTLED';
|
||||||
}
|
}
|
||||||
function settleButtonLabel(row: unknown) {
|
function settleButtonLabel(row: unknown) {
|
||||||
return matchStatus(row) === 'SETTLED' ? t('common.resettle') : t('common.settle');
|
return matchStatus(row) === 'SETTLED' ? t('common.resettle') : t('common.settle');
|
||||||
|
|||||||
@@ -1925,24 +1925,12 @@ export class AdminController {
|
|||||||
@Body() dto?: SettlementPreviewDto,
|
@Body() dto?: SettlementPreviewDto,
|
||||||
) {
|
) {
|
||||||
const matchId = BigInt(id);
|
const matchId = BigInt(id);
|
||||||
const hasScore =
|
|
||||||
dto?.htHome !== undefined ||
|
|
||||||
dto?.htAway !== undefined ||
|
|
||||||
dto?.ftHome !== undefined ||
|
|
||||||
dto?.ftAway !== undefined ||
|
|
||||||
dto?.winnerTeamId !== undefined;
|
|
||||||
if (hasScore) {
|
|
||||||
await this.settlement.recordScore(
|
|
||||||
matchId,
|
|
||||||
dto!.htHome ?? 0,
|
|
||||||
dto!.htAway ?? 0,
|
|
||||||
dto!.ftHome ?? 0,
|
|
||||||
dto!.ftAway ?? 0,
|
|
||||||
operatorId,
|
|
||||||
dto!.winnerTeamId != null ? BigInt(dto!.winnerTeamId) : undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const preview = await this.settlement.previewSettlement(matchId, operatorId, {
|
const preview = await this.settlement.previewSettlement(matchId, operatorId, {
|
||||||
|
htHome: dto?.htHome,
|
||||||
|
htAway: dto?.htAway,
|
||||||
|
ftHome: dto?.ftHome,
|
||||||
|
ftAway: dto?.ftAway,
|
||||||
|
winnerTeamId: dto?.winnerTeamId != null ? BigInt(dto.winnerTeamId) : undefined,
|
||||||
page: dto?.page ? Math.max(1, dto.page) : 1,
|
page: dto?.page ? Math.max(1, dto.page) : 1,
|
||||||
pageSize: dto?.pageSize ? Math.min(100, Math.max(1, dto.pageSize)) : 10,
|
pageSize: dto?.pageSize ? Math.min(100, Math.max(1, dto.pageSize)) : 10,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1104,11 +1104,21 @@ export class MatchesService {
|
|||||||
async reopenMatch(matchId: bigint, startTime?: Date) {
|
async reopenMatch(matchId: bigint, startTime?: Date) {
|
||||||
const match = await this.requireAdminMatch(matchId);
|
const match = await this.requireAdminMatch(matchId);
|
||||||
if (match.isOutright) throw appBadRequest('OUTRIGHT_EDIT_VIA_MARKETS');
|
if (match.isOutright) throw appBadRequest('OUTRIGHT_EDIT_VIA_MARKETS');
|
||||||
if (match.status !== 'CLOSED') throw appBadRequest('MATCH_NOT_REOPENABLE');
|
|
||||||
|
|
||||||
const scoreRow = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
const scoreRow = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
||||||
if (scoreRow) throw appBadRequest('MATCH_NOT_REOPENABLE');
|
if (scoreRow) throw appBadRequest('MATCH_NOT_REOPENABLE');
|
||||||
|
|
||||||
|
const reopenable =
|
||||||
|
match.status === 'CLOSED' ||
|
||||||
|
(match.status === 'PENDING_SETTLEMENT' && !scoreRow);
|
||||||
|
if (!reopenable) throw appBadRequest('MATCH_NOT_REOPENABLE');
|
||||||
|
|
||||||
|
if (match.status === 'PENDING_SETTLEMENT') {
|
||||||
|
await this.prisma.settlementBatch.deleteMany({
|
||||||
|
where: { matchId, status: 'PREVIEW' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const effectiveStart = startTime ?? match.startTime;
|
const effectiveStart = startTime ?? match.startTime;
|
||||||
if (!isPreMatchKickoff(effectiveStart)) {
|
if (!isPreMatchKickoff(effectiveStart)) {
|
||||||
throw appBadRequest('MATCH_REOPEN_KICKOFF_REQUIRED');
|
throw appBadRequest('MATCH_REOPEN_KICKOFF_REQUIRED');
|
||||||
|
|||||||
@@ -27,15 +27,16 @@ async function confirmMatchSettlement(
|
|||||||
fx: BetFlowFixtureIds,
|
fx: BetFlowFixtureIds,
|
||||||
score: { htHome: number; htAway: number; ftHome: number; ftAway: number },
|
score: { htHome: number; htAway: number; ftHome: number; ftAway: number },
|
||||||
) {
|
) {
|
||||||
await deps.settlement.recordScore(
|
await deps.prisma.match.update({
|
||||||
fx.matchId,
|
where: { id: fx.matchId },
|
||||||
score.htHome,
|
data: { status: 'CLOSED', closeTime: new Date() },
|
||||||
score.htAway,
|
});
|
||||||
score.ftHome,
|
const preview = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId, {
|
||||||
score.ftAway,
|
htHome: score.htHome,
|
||||||
fx.operatorId,
|
htAway: score.htAway,
|
||||||
);
|
ftHome: score.ftHome,
|
||||||
const preview = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId);
|
ftAway: score.ftAway,
|
||||||
|
});
|
||||||
await deps.settlement.confirmSettlement(preview.batch.id, fx.operatorId);
|
await deps.settlement.confirmSettlement(preview.batch.id, fx.operatorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
templateScoresForMarket,
|
templateScoresForMarket,
|
||||||
} from './domain/settlement-helpers';
|
} from './domain/settlement-helpers';
|
||||||
|
|
||||||
|
const SETTLEMENT_ENTRY_STATUSES = new Set(['CLOSED', 'PENDING_SETTLEMENT', 'SETTLED']);
|
||||||
|
|
||||||
type BetSelectionLeg = {
|
type BetSelectionLeg = {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
matchId: bigint | null;
|
matchId: bigint | null;
|
||||||
@@ -84,6 +86,12 @@ export class SettlementService {
|
|||||||
return 'WON';
|
return 'WON';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assertMatchClosedForSettlement(status: string) {
|
||||||
|
if (!SETTLEMENT_ENTRY_STATUSES.has(status)) {
|
||||||
|
throw appBadRequest('MATCH_MUST_CLOSE_FOR_SETTLEMENT');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async recordScore(
|
async recordScore(
|
||||||
matchId: bigint,
|
matchId: bigint,
|
||||||
htHome: number,
|
htHome: number,
|
||||||
@@ -97,6 +105,7 @@ export class SettlementService {
|
|||||||
where: { id: matchId, deletedAt: null },
|
where: { id: matchId, deletedAt: null },
|
||||||
});
|
});
|
||||||
if (!match) throw appNotFound('MATCH_NOT_FOUND');
|
if (!match) throw appNotFound('MATCH_NOT_FOUND');
|
||||||
|
this.assertMatchClosedForSettlement(match.status);
|
||||||
|
|
||||||
if (match.isOutright) {
|
if (match.isOutright) {
|
||||||
if (!winnerTeamId) {
|
if (!winnerTeamId) {
|
||||||
@@ -147,20 +156,32 @@ export class SettlementService {
|
|||||||
async previewSettlement(
|
async previewSettlement(
|
||||||
matchId: bigint,
|
matchId: bigint,
|
||||||
operatorId: bigint,
|
operatorId: bigint,
|
||||||
opts?: { page?: number; pageSize?: number },
|
opts?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
htHome?: number;
|
||||||
|
htAway?: number;
|
||||||
|
ftHome?: number;
|
||||||
|
ftAway?: number;
|
||||||
|
winnerTeamId?: bigint;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const score = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
const match = await this.prisma.match.findFirst({
|
||||||
if (!score) throw appBadRequest('SCORE_NOT_RECORDED');
|
where: { id: matchId, deletedAt: null },
|
||||||
|
});
|
||||||
|
if (!match) throw appNotFound('MATCH_NOT_FOUND');
|
||||||
|
this.assertMatchClosedForSettlement(match.status);
|
||||||
|
|
||||||
const computation = await this.computePreviewComputation(matchId);
|
const scoreSource = await this.resolvePreviewScoreSource(matchId, match.isOutright, opts);
|
||||||
|
const computation = await this.computePreviewComputation(matchId, scoreSource);
|
||||||
const batch = await this.prisma.settlementBatch.create({
|
const batch = await this.prisma.settlementBatch.create({
|
||||||
data: {
|
data: {
|
||||||
matchId,
|
matchId,
|
||||||
batchNo: generateBatchNo('STL'),
|
batchNo: generateBatchNo('STL'),
|
||||||
htHomeScore: score.htHomeScore,
|
htHomeScore: scoreSource.htHome,
|
||||||
htAwayScore: score.htAwayScore,
|
htAwayScore: scoreSource.htAway,
|
||||||
ftHomeScore: score.ftHomeScore,
|
ftHomeScore: scoreSource.ftHome,
|
||||||
ftAwayScore: score.ftAwayScore,
|
ftAwayScore: scoreSource.ftAway,
|
||||||
status: 'PREVIEW',
|
status: 'PREVIEW',
|
||||||
totalBets: computation.pendingBets.length,
|
totalBets: computation.pendingBets.length,
|
||||||
totalPayout: computation.totalPayout,
|
totalPayout: computation.totalPayout,
|
||||||
@@ -169,6 +190,13 @@ export class SettlementService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (match.status !== 'PENDING_SETTLEMENT' && match.status !== 'SETTLED') {
|
||||||
|
await this.prisma.match.update({
|
||||||
|
where: { id: matchId },
|
||||||
|
data: { status: 'PENDING_SETTLEMENT' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.buildPreviewResponse(computation, batch, opts);
|
return this.buildPreviewResponse(computation, batch, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +210,16 @@ export class SettlementService {
|
|||||||
throw appBadRequest('SETTLEMENT_BATCH_NOT_PREVIEW');
|
throw appBadRequest('SETTLEMENT_BATCH_NOT_PREVIEW');
|
||||||
}
|
}
|
||||||
|
|
||||||
const computation = await this.computePreviewComputation(batch.matchId);
|
const existingScore = await this.prisma.matchScore.findUnique({
|
||||||
|
where: { matchId: batch.matchId },
|
||||||
|
});
|
||||||
|
const computation = await this.computePreviewComputation(batch.matchId, {
|
||||||
|
htHome: batch.htHomeScore ?? 0,
|
||||||
|
htAway: batch.htAwayScore ?? 0,
|
||||||
|
ftHome: batch.ftHomeScore ?? 0,
|
||||||
|
ftAway: batch.ftAwayScore ?? 0,
|
||||||
|
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||||
|
});
|
||||||
const itemsPage = this.paginatePreviewItems(computation.items, opts);
|
const itemsPage = this.paginatePreviewItems(computation.items, opts);
|
||||||
return {
|
return {
|
||||||
items: itemsPage.items.map((item) => this.serializePreviewItem(item)),
|
items: itemsPage.items.map((item) => this.serializePreviewItem(item)),
|
||||||
@@ -269,17 +306,90 @@ export class SettlementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async computePreviewComputation(matchId: bigint) {
|
private async resolvePreviewScoreSource(
|
||||||
|
matchId: bigint,
|
||||||
|
isOutright: boolean,
|
||||||
|
opts?: {
|
||||||
|
htHome?: number;
|
||||||
|
htAway?: number;
|
||||||
|
ftHome?: number;
|
||||||
|
ftAway?: number;
|
||||||
|
winnerTeamId?: bigint;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const hasRequestScore =
|
||||||
|
opts?.htHome !== undefined ||
|
||||||
|
opts?.htAway !== undefined ||
|
||||||
|
opts?.ftHome !== undefined ||
|
||||||
|
opts?.ftAway !== undefined ||
|
||||||
|
opts?.winnerTeamId !== undefined;
|
||||||
|
|
||||||
|
if (hasRequestScore) {
|
||||||
|
if (isOutright) {
|
||||||
|
if (!opts?.winnerTeamId) throw appBadRequest('SETTLEMENT_WINNER_REQUIRED');
|
||||||
|
const team = await this.prisma.team.findUnique({ where: { id: opts.winnerTeamId } });
|
||||||
|
if (!team) throw appBadRequest('SETTLEMENT_WINNER_NOT_FOUND');
|
||||||
|
const outrightSel = await this.prisma.marketSelection.findFirst({
|
||||||
|
where: {
|
||||||
|
market: { matchId, marketType: 'OUTRIGHT_WINNER' },
|
||||||
|
selectionCode: team.code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!outrightSel) throw appBadRequest('SETTLEMENT_WINNER_NOT_IN_MARKET');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
htHome: opts?.htHome ?? 0,
|
||||||
|
htAway: opts?.htAway ?? 0,
|
||||||
|
ftHome: opts?.ftHome ?? 0,
|
||||||
|
ftAway: opts?.ftAway ?? 0,
|
||||||
|
winnerTeamId: isOutright ? (opts?.winnerTeamId ?? null) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const score = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
const score = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
||||||
if (!score) throw appBadRequest('SCORE_NOT_RECORDED');
|
if (!score) throw appBadRequest('SCORE_NOT_RECORDED');
|
||||||
|
|
||||||
const scoreInput: ScoreInput = {
|
return {
|
||||||
|
htHome: score.htHomeScore ?? 0,
|
||||||
|
htAway: score.htAwayScore ?? 0,
|
||||||
|
ftHome: score.ftHomeScore ?? 0,
|
||||||
|
ftAway: score.ftAwayScore ?? 0,
|
||||||
|
winnerTeamId: score.winnerTeamId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async computePreviewComputation(
|
||||||
|
matchId: bigint,
|
||||||
|
scoreSource?: {
|
||||||
|
htHome: number;
|
||||||
|
htAway: number;
|
||||||
|
ftHome: number;
|
||||||
|
ftAway: number;
|
||||||
|
winnerTeamId?: bigint | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
let scoreInput: ScoreInput;
|
||||||
|
let winnerTeamCode: string | null;
|
||||||
|
|
||||||
|
if (scoreSource) {
|
||||||
|
scoreInput = {
|
||||||
|
htHome: scoreSource.htHome,
|
||||||
|
htAway: scoreSource.htAway,
|
||||||
|
ftHome: scoreSource.ftHome,
|
||||||
|
ftAway: scoreSource.ftAway,
|
||||||
|
};
|
||||||
|
winnerTeamCode = await this.resolveWinnerTeamCode(scoreSource.winnerTeamId ?? null);
|
||||||
|
} else {
|
||||||
|
const score = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
||||||
|
if (!score) throw appBadRequest('SCORE_NOT_RECORDED');
|
||||||
|
scoreInput = {
|
||||||
htHome: score.htHomeScore ?? 0,
|
htHome: score.htHomeScore ?? 0,
|
||||||
htAway: score.htAwayScore ?? 0,
|
htAway: score.htAwayScore ?? 0,
|
||||||
ftHome: score.ftHomeScore ?? 0,
|
ftHome: score.ftHomeScore ?? 0,
|
||||||
ftAway: score.ftAwayScore ?? 0,
|
ftAway: score.ftAwayScore ?? 0,
|
||||||
};
|
};
|
||||||
const winnerTeamCode = await this.resolveWinnerTeamCode(score.winnerTeamId);
|
winnerTeamCode = await this.resolveWinnerTeamCode(score.winnerTeamId);
|
||||||
|
}
|
||||||
|
|
||||||
const pendingBets = await this.prisma.bet.findMany({
|
const pendingBets = await this.prisma.bet.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -487,19 +597,20 @@ export class SettlementService {
|
|||||||
});
|
});
|
||||||
if (!batch) throw appNotFound('SETTLEMENT_BATCH_NOT_FOUND');
|
if (!batch) throw appNotFound('SETTLEMENT_BATCH_NOT_FOUND');
|
||||||
if (batch.status !== 'PREVIEW') throw appBadRequest('SETTLEMENT_BATCH_ALREADY_CONFIRMED');
|
if (batch.status !== 'PREVIEW') throw appBadRequest('SETTLEMENT_BATCH_ALREADY_CONFIRMED');
|
||||||
|
if (batch.match.status !== 'PENDING_SETTLEMENT') {
|
||||||
const score = await this.prisma.matchScore.findUnique({
|
throw appBadRequest('MATCH_NOT_SETTLEABLE');
|
||||||
where: { matchId: batch.matchId },
|
}
|
||||||
});
|
|
||||||
if (!score) throw appBadRequest('SCORE_NOT_FOUND');
|
|
||||||
|
|
||||||
const scoreInput: ScoreInput = {
|
const scoreInput: ScoreInput = {
|
||||||
htHome: score.htHomeScore ?? 0,
|
htHome: batch.htHomeScore ?? 0,
|
||||||
htAway: score.htAwayScore ?? 0,
|
htAway: batch.htAwayScore ?? 0,
|
||||||
ftHome: score.ftHomeScore ?? 0,
|
ftHome: batch.ftHomeScore ?? 0,
|
||||||
ftAway: score.ftAwayScore ?? 0,
|
ftAway: batch.ftAwayScore ?? 0,
|
||||||
};
|
};
|
||||||
const winnerTeamCode = await this.resolveWinnerTeamCode(score.winnerTeamId);
|
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({
|
const pendingBets = await this.prisma.bet.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -513,6 +624,26 @@ export class SettlementService {
|
|||||||
const agentIds = new Set<bigint>();
|
const agentIds = new Set<bigint>();
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.matchScore.upsert({
|
||||||
|
where: { matchId: batch.matchId },
|
||||||
|
create: {
|
||||||
|
matchId: batch.matchId,
|
||||||
|
htHomeScore: scoreInput.htHome,
|
||||||
|
htAwayScore: scoreInput.htAway,
|
||||||
|
ftHomeScore: scoreInput.ftHome,
|
||||||
|
ftAwayScore: scoreInput.ftAway,
|
||||||
|
winnerTeamId: existingScore?.winnerTeamId ?? null,
|
||||||
|
recordedBy: operatorId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
htHomeScore: scoreInput.htHome,
|
||||||
|
htAwayScore: scoreInput.htAway,
|
||||||
|
ftHomeScore: scoreInput.ftHome,
|
||||||
|
ftAwayScore: scoreInput.ftAway,
|
||||||
|
recordedBy: operatorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
for (const bet of pendingBets) {
|
for (const bet of pendingBets) {
|
||||||
if (bet.betType === 'SINGLE' && bet.selections.length === 1) {
|
if (bet.betType === 'SINGLE' && bet.selections.length === 1) {
|
||||||
const sel = bet.selections[0];
|
const sel = bet.selections[0];
|
||||||
@@ -710,6 +841,7 @@ export class SettlementService {
|
|||||||
where: { id: matchId, deletedAt: null },
|
where: { id: matchId, deletedAt: null },
|
||||||
});
|
});
|
||||||
if (!match) throw appNotFound('MATCH_NOT_FOUND');
|
if (!match) throw appNotFound('MATCH_NOT_FOUND');
|
||||||
|
this.assertMatchClosedForSettlement(match.status);
|
||||||
|
|
||||||
const legs = await this.prisma.betSelection.findMany({
|
const legs = await this.prisma.betSelection.findMany({
|
||||||
where: { matchId },
|
where: { matchId },
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ export const API_ERROR_MESSAGES = {
|
|||||||
'en-US': 'Match cannot be reopened in current status',
|
'en-US': 'Match cannot be reopened in current status',
|
||||||
'ms-MY': 'Perlawanan tidak boleh dibuka semula dalam status semasa',
|
'ms-MY': 'Perlawanan tidak boleh dibuka semula dalam status semasa',
|
||||||
},
|
},
|
||||||
|
MATCH_NOT_SETTLEABLE: {
|
||||||
|
'zh-CN': '当前状态不可确认结算',
|
||||||
|
'en-US': 'Match cannot be settled in current status',
|
||||||
|
'ms-MY': 'Perlawanan tidak boleh diselesaikan dalam status semasa',
|
||||||
|
},
|
||||||
|
MATCH_MUST_CLOSE_FOR_SETTLEMENT: {
|
||||||
|
'zh-CN': '请先封盘后再结算',
|
||||||
|
'en-US': 'Close betting before settlement',
|
||||||
|
'ms-MY': 'Tutup pertaruhan sebelum penyelesaian',
|
||||||
|
},
|
||||||
MATCH_REOPEN_KICKOFF_REQUIRED: {
|
MATCH_REOPEN_KICKOFF_REQUIRED: {
|
||||||
'zh-CN': '开赛时间已过,请设置新的未来开赛时间',
|
'zh-CN': '开赛时间已过,请设置新的未来开赛时间',
|
||||||
'en-US': 'Kickoff has passed; set a new future start time',
|
'en-US': 'Kickoff has passed; set a new future start time',
|
||||||
|
|||||||
Reference in New Issue
Block a user