This commit is contained in:
wchino
2026-06-13 17:38:25 +08:00
parent e7e938f261
commit 7b33d9f9fa
190 changed files with 23222 additions and 4336 deletions

View File

@@ -15,7 +15,7 @@ describe('CatalogArchiveService', () => {
entityTranslation: { findFirst: jest.Mock };
$transaction: jest.Mock;
};
let matches: { betStatsForMatches: jest.Mock };
let matchBetStats: { betStatsForMatches: jest.Mock };
let service: CatalogArchiveService;
beforeEach(() => {
@@ -34,8 +34,8 @@ describe('CatalogArchiveService', () => {
entityTranslation: { findFirst: jest.fn().mockResolvedValue(null) },
$transaction: jest.fn(async (fn: (tx: typeof prisma) => Promise<void>) => fn(prisma)),
};
matches = { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) };
service = new CatalogArchiveService(prisma as never, matches as never);
matchBetStats = { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) };
service = new CatalogArchiveService(prisma as never, matchBetStats as never);
});
const baseMatch = {
@@ -71,21 +71,13 @@ describe('CatalogArchiveService', () => {
});
});
it('archive with force soft-deletes and cancels match', async () => {
it('archive with force rejects draft matches', async () => {
prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'DRAFT' });
prisma.match.update.mockResolvedValue({});
const result = await service.archiveMatch(matchId, { force: true });
expect(result.matchId).toBe(matchId.toString());
expect(prisma.marketSelection.updateMany).toHaveBeenCalled();
expect(prisma.market.updateMany).toHaveBeenCalled();
expect(prisma.match.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: matchId },
data: expect.objectContaining({ status: 'CANCELLED', deletedAt: expect.any(Date) }),
}),
);
await expect(service.archiveMatch(matchId, { force: true })).rejects.toMatchObject({
response: expect.objectContaining({ code: 'MATCH_DELETE_DRAFT_ONLY' }),
});
expect(prisma.match.update).not.toHaveBeenCalled();
});
it('league preview blocks when child match is not terminal', async () => {
@@ -102,7 +94,7 @@ describe('CatalogArchiveService', () => {
awayTeam: { code: 'B' },
},
]);
matches.betStatsForMatches.mockResolvedValue(
matchBetStats.betStatsForMatches.mockResolvedValue(
new Map([[matchId.toString(), { betCount: 0, totalStake: '0', pendingCount: 0 }]]),
);
@@ -129,7 +121,7 @@ describe('CatalogArchiveService', () => {
},
])
.mockResolvedValueOnce([{ id: matchId, status: 'SETTLED' }]);
matches.betStatsForMatches.mockResolvedValue(
matchBetStats.betStatsForMatches.mockResolvedValue(
new Map([[matchId.toString(), { betCount: 1, totalStake: '100', pendingCount: 0 }]]),
);
@@ -163,7 +155,7 @@ describe('CatalogArchiveService', () => {
awayTeam: { code: 'B' },
},
]);
matches.betStatsForMatches.mockResolvedValue(
matchBetStats.betStatsForMatches.mockResolvedValue(
new Map([[matchId.toString(), { betCount: 0, totalStake: '0', pendingCount: 0 }]]),
);

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest, appConflict, appNotFound } from '../../shared/common/app-error';
import { MatchesService } from './matches.service';
import { MatchBetStatsService } from './match-bet-stats.service';
const TERMINAL_MATCH_STATUSES = new Set(['SETTLED', 'CANCELLED', 'VOID']);
@@ -39,7 +39,7 @@ export type LeagueArchivePreview = {
export class CatalogArchiveService {
constructor(
private prisma: PrismaService,
private matches: MatchesService,
private matchBetStats: MatchBetStatsService,
) {}
async getMatchArchivePreview(matchId: bigint): Promise<MatchArchivePreview> {
@@ -114,7 +114,7 @@ export class CatalogArchiveService {
});
const matchIds = matches.map((m) => m.id);
const stats = await this.matches.betStatsForMatches(matchIds);
const stats = await this.matchBetStats.betStatsForMatches(matchIds);
const previewBatches = matchIds.length
? await this.prisma.settlementBatch.findMany({
where: { matchId: { in: matchIds }, status: 'PREVIEW' },

View File

@@ -0,0 +1,71 @@
import { Decimal } from '@prisma/client/runtime/library';
import { MatchBetStatsService } from './match-bet-stats.service';
describe('MatchBetStatsService', () => {
let prisma: {
betSelection: { findMany: jest.Mock };
};
let service: MatchBetStatsService;
beforeEach(() => {
prisma = {
betSelection: { findMany: jest.fn().mockResolvedValue([]) },
};
service = new MatchBetStatsService(prisma as never);
});
it('returns empty stats without querying when match ids are empty', async () => {
const stats = await service.betStatsForMatches([]);
expect(stats.size).toBe(0);
expect(prisma.betSelection.findMany).not.toHaveBeenCalled();
});
it('deduplicates bets and fills zero stats for matches without bets', async () => {
const matchId = BigInt(10);
const emptyMatchId = BigInt(11);
prisma.betSelection.findMany.mockResolvedValue([
{
matchId,
betId: BigInt(100),
bet: { stake: new Decimal(50), status: 'PENDING' },
},
{
matchId,
betId: BigInt(100),
bet: { stake: new Decimal(50), status: 'PENDING' },
},
{
matchId,
betId: BigInt(101),
bet: { stake: new Decimal(25), status: 'WON' },
},
{
matchId: null,
betId: BigInt(102),
bet: { stake: new Decimal(99), status: 'PENDING' },
},
]);
const stats = await service.betStatsForMatches([matchId, emptyMatchId]);
expect(prisma.betSelection.findMany).toHaveBeenCalledWith({
where: { matchId: { in: [matchId, emptyMatchId] } },
select: {
matchId: true,
betId: true,
bet: { select: { stake: true, status: true } },
},
});
expect(stats.get(matchId.toString())).toEqual({
betCount: 2,
totalStake: '75',
pendingCount: 1,
});
expect(stats.get(emptyMatchId.toString())).toEqual({
betCount: 0,
totalStake: '0',
pendingCount: 0,
});
});
});

View File

@@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
export type MatchBetStatsSummary = {
betCount: number;
totalStake: string;
pendingCount: number;
};
@Injectable()
export class MatchBetStatsService {
constructor(private prisma: PrismaService) {}
/** 批量汇总多场关联注单(按 bet 去重计注单数) */
async betStatsForMatches(
matchIds: bigint[],
): Promise<Map<string, MatchBetStatsSummary>> {
const result = new Map<string, MatchBetStatsSummary>();
if (!matchIds.length) return result;
const legs = await this.prisma.betSelection.findMany({
where: { matchId: { in: matchIds } },
select: {
matchId: true,
betId: true,
bet: { select: { stake: true, status: true } },
},
});
const byMatch = new Map<
string,
Map<string, { stake: Decimal; status: string }>
>();
for (const leg of legs) {
if (leg.matchId == null) continue;
const mid = leg.matchId.toString();
if (!byMatch.has(mid)) byMatch.set(mid, new Map());
const bets = byMatch.get(mid)!;
if (!bets.has(leg.betId.toString())) {
bets.set(leg.betId.toString(), {
stake: leg.bet.stake,
status: leg.bet.status,
});
}
}
for (const id of matchIds) {
const mid = id.toString();
const bets = byMatch.get(mid);
if (!bets) {
result.set(mid, {
betCount: 0,
totalStake: '0',
pendingCount: 0,
});
continue;
}
let totalStake = new Decimal(0);
let pendingCount = 0;
for (const b of bets.values()) {
totalStake = totalStake.add(b.stake);
if (b.status === 'PENDING') pendingCount += 1;
}
result.set(mid, {
betCount: bets.size,
totalStake: totalStake.toString(),
pendingCount,
});
}
return result;
}
}

View File

@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { MarketsModule } from '../odds/markets.module';
import { CatalogArchiveService } from './catalog-archive.service';
import { MatchBetStatsService } from './match-bet-stats.service';
import { MatchesService } from './matches.service';
import { OutrightService } from './outright.service';
@Module({
imports: [MarketsModule],
providers: [MatchesService, OutrightService, CatalogArchiveService],
exports: [MatchesService, OutrightService, CatalogArchiveService],
providers: [MatchesService, MatchBetStatsService, OutrightService, CatalogArchiveService],
exports: [MatchesService, MatchBetStatsService, OutrightService, CatalogArchiveService],
})
export class MatchesModule {}

View File

@@ -1,3 +1,12 @@
jest.mock('@thebet365/shared', () => ({
isPreMatchKickoff: jest.fn(() => true),
PARLAY_MARKET_TYPES: [],
resolveTranslationFallback: jest.fn(
(translations: Map<string, string>, locale: string) =>
translations.get(locale) ?? translations.get('zh-CN') ?? translations.get('en-US') ?? null,
),
}));
import { MatchesService } from './matches.service';
describe('MatchesService publish/unpublish', () => {
@@ -11,6 +20,7 @@ describe('MatchesService publish/unpublish', () => {
settlementBatch: { deleteMany: jest.Mock };
};
let outright: { syncWithLeaguePublished: jest.Mock };
let matchBetStats: { betStatsForMatches: jest.Mock };
let service: MatchesService;
beforeEach(() => {
@@ -28,7 +38,8 @@ describe('MatchesService publish/unpublish', () => {
settlementBatch: { deleteMany: jest.fn().mockResolvedValue({ count: 0 }) },
};
outright = { syncWithLeaguePublished: jest.fn().mockResolvedValue(undefined) };
service = new MatchesService(prisma as never, outright as never);
matchBetStats = { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) };
service = new MatchesService(prisma as never, outright as never, matchBetStats as never);
});
describe('updatePlatformLeague unpublish', () => {

View File

@@ -1,16 +1,20 @@
import { Injectable } from '@nestjs/common';
import { isPreMatchKickoff, MarketType, resolveTranslationFallback } from '@thebet365/shared';
import {
defaultMarketName,
defaultSelectionName,
isPreMatchKickoff,
isSettlementSupportedMarketType,
PARLAY_MARKET_TYPES,
resolveMarketText,
resolveTranslationFallback,
sanitizeLocalizedText,
} from '@thebet365/shared';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
export type MatchBetStatsSummary = {
betCount: number;
totalStake: string;
pendingCount: number;
};
import { MatchBetStatsService, type MatchBetStatsSummary } from './match-bet-stats.service';
import type { ZhiboLeagueExport, ZhiboMatchExport, ZhiboMatchesBundleExport, ZhiboTeamExport } from './zhibo-match.types';
import {
leagueCodeFromExport,
@@ -25,17 +29,11 @@ import {
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
import { OutrightService } from './outright.service';
export type { MatchBetStatsSummary } from './match-bet-stats.service';
const OUTRIGHT_PLACEHOLDER_CODE = 'OUT';
const PLAYER_PARLAY_MARKET_TYPES = [
MarketType.FT_HANDICAP,
MarketType.FT_OVER_UNDER,
MarketType.FT_1X2,
MarketType.FT_ODD_EVEN,
MarketType.HT_HANDICAP,
MarketType.HT_OVER_UNDER,
MarketType.HT_1X2,
] as const;
const PLAYER_PARLAY_MARKET_TYPES = PARLAY_MARKET_TYPES;
export type ListPublishedOptions = {
/** 响应中是否包含 markets列表页默认 false仅摘要 */
@@ -49,6 +47,7 @@ export class MatchesService {
constructor(
private prisma: PrismaService,
private outright: OutrightService,
private matchBetStats: MatchBetStatsService,
) {}
async createLeague(code: string, translations: Record<string, string>) {
@@ -89,7 +88,6 @@ export class MatchesService {
awayTeamId: bigint;
startTime: Date;
isHot?: boolean;
correctScoreEnabled?: boolean;
displayOrder?: number;
createdBy?: bigint;
status?: string;
@@ -115,7 +113,6 @@ export class MatchesService {
awayTeamId: data.awayTeamId,
startTime: data.startTime,
isHot: data.isHot ?? false,
correctScoreEnabled: data.correctScoreEnabled ?? true,
displayOrder: data.displayOrder ?? 0,
createdBy: data.createdBy,
status,
@@ -624,59 +621,7 @@ export class MatchesService {
async betStatsForMatches(
matchIds: bigint[],
): Promise<Map<string, MatchBetStatsSummary>> {
const result = new Map<string, MatchBetStatsSummary>();
if (!matchIds.length) return result;
const legs = await this.prisma.betSelection.findMany({
where: { matchId: { in: matchIds } },
select: {
matchId: true,
betId: true,
bet: { select: { stake: true, status: true } },
},
});
const byMatch = new Map<
string,
Map<string, { stake: Decimal; status: string }>
>();
for (const leg of legs) {
if (leg.matchId == null) continue;
const mid = leg.matchId.toString();
if (!byMatch.has(mid)) byMatch.set(mid, new Map());
const bets = byMatch.get(mid)!;
if (!bets.has(leg.betId.toString())) {
bets.set(leg.betId.toString(), {
stake: leg.bet.stake,
status: leg.bet.status,
});
}
}
for (const id of matchIds) {
const mid = id.toString();
const bets = byMatch.get(mid);
if (!bets) {
result.set(mid, {
betCount: 0,
totalStake: '0',
pendingCount: 0,
});
continue;
}
let totalStake = new Decimal(0);
let pendingCount = 0;
for (const b of bets.values()) {
totalStake = totalStake.add(b.stake);
if (b.status === 'PENDING') pendingCount += 1;
}
result.set(mid, {
betCount: bets.size,
totalStake: totalStake.toString(),
pendingCount,
});
}
return result;
return this.matchBetStats.betStatsForMatches(matchIds);
}
private async upsertTeamByCode(data: {
@@ -718,7 +663,6 @@ export class MatchesService {
awayTeamMs?: string;
startTime: Date;
isHot?: boolean;
correctScoreEnabled?: boolean;
displayOrder?: number;
matchName?: string;
stage?: string;
@@ -833,7 +777,6 @@ export class MatchesService {
awayTeamId: awayTeam.id,
startTime: data.startTime,
isHot: data.isHot ?? false,
correctScoreEnabled: data.correctScoreEnabled ?? true,
displayOrder: data.displayOrder ?? 0,
createdBy: data.createdBy,
status: 'DRAFT',
@@ -881,7 +824,6 @@ export class MatchesService {
status: match.status,
isOutright: match.isOutright,
isHot: match.isHot,
correctScoreEnabled: match.correctScoreEnabled,
displayOrder: match.displayOrder,
startTime: match.startTime.toISOString(),
leagueId: match.leagueId.toString(),
@@ -909,21 +851,36 @@ export class MatchesService {
htAway: scoreRow.htAwayScore ?? 0,
ftHome: scoreRow.ftHomeScore ?? 0,
ftAway: scoreRow.ftAwayScore ?? 0,
homeCorners: scoreRow.homeCorners ?? null,
awayCorners: scoreRow.awayCorners ?? null,
homeYellowCards: scoreRow.homeYellowCards ?? null,
awayYellowCards: scoreRow.awayYellowCards ?? null,
homeRedCards: scoreRow.homeRedCards ?? null,
awayRedCards: scoreRow.awayRedCards ?? null,
homeCards: scoreRow.homeCards ?? null,
awayCards: scoreRow.awayCards ?? null,
winnerTeamId: scoreRow.winnerTeamId?.toString() ?? null,
}
: null,
markets: markets.map((m) => ({
id: m.id.toString(),
marketType: m.marketType,
marketKey: m.marketKey ?? m.marketType,
lineKey: m.lineKey ?? null,
period: m.period,
lineValue: m.lineValue != null ? Number(m.lineValue) : null,
paramsJson: m.paramsJson ?? null,
status: m.status,
showOnPlayer: m.showOnPlayer,
promoLabel: m.promoLabel ?? '',
promoLabelI18n: sanitizeLocalizedText(m.promoLabelI18n),
nameI18n: sanitizeLocalizedText(m.nameI18n),
sortOrder: m.sortOrder,
selections: m.selections.map((s) => ({
id: s.id.toString(),
selectionCode: s.selectionCode,
selectionName: s.selectionName,
nameI18n: sanitizeLocalizedText(s.nameI18n),
odds: Number(s.odds),
status: s.status,
sortOrder: s.sortOrder,
@@ -949,7 +906,6 @@ export class MatchesService {
groupName?: string;
homeTeamLogoUrl?: string;
awayTeamLogoUrl?: string;
correctScoreEnabled?: boolean;
updatedBy?: bigint;
},
) {
@@ -1006,7 +962,6 @@ export class MatchesService {
matchName,
stage: data.stage !== undefined ? data.stage.trim() || null : match.stage,
groupName: data.groupName !== undefined ? data.groupName.trim() || null : match.groupName,
correctScoreEnabled: data.correctScoreEnabled ?? match.correctScoreEnabled,
updatedBy: data.updatedBy,
},
});
@@ -1261,7 +1216,6 @@ export class MatchesService {
startTime: Date;
status?: string;
isHot?: boolean;
correctScoreEnabled?: boolean;
displayOrder?: number;
matchName?: string | null;
stage?: string | null;
@@ -1270,10 +1224,18 @@ export class MatchesService {
awayTeam?: { code: string; logoUrl?: string | null };
league?: { logoUrl?: string | null };
score?: {
htHomeScore: number;
htAwayScore: number;
ftHomeScore: number;
ftAwayScore: number;
htHomeScore: number | null;
htAwayScore: number | null;
ftHomeScore: number | null;
ftAwayScore: number | null;
homeCorners?: number | null;
awayCorners?: number | null;
homeYellowCards?: number | null;
awayYellowCards?: number | null;
homeRedCards?: number | null;
awayRedCards?: number | null;
homeCards?: number | null;
awayCards?: number | null;
} | null;
markets?: Array<Record<string, unknown>>;
};
@@ -1295,7 +1257,6 @@ export class MatchesService {
awayTeamLogoUrl: m.awayTeam?.logoUrl ?? null,
startTime: m.startTime.toISOString(),
isHot: m.isHot ?? false,
correctScoreEnabled: m.correctScoreEnabled ?? true,
displayOrder: m.displayOrder ?? 0,
matchName: m.matchName ?? null,
stage: m.stage ?? null,
@@ -1303,10 +1264,18 @@ export class MatchesService {
status: m.status ?? 'PUBLISHED',
score: m.score
? {
htHome: m.score.htHomeScore,
htAway: m.score.htAwayScore,
ftHome: m.score.ftHomeScore,
ftAway: m.score.ftAwayScore,
htHome: m.score.htHomeScore ?? null,
htAway: m.score.htAwayScore ?? null,
ftHome: m.score.ftHomeScore ?? null,
ftAway: m.score.ftAwayScore ?? null,
homeCorners: m.score.homeCorners ?? null,
awayCorners: m.score.awayCorners ?? null,
homeYellowCards: m.score.homeYellowCards ?? null,
awayYellowCards: m.score.awayYellowCards ?? null,
homeRedCards: m.score.homeRedCards ?? null,
awayRedCards: m.score.awayRedCards ?? null,
homeCards: m.score.homeCards ?? null,
awayCards: m.score.awayCards ?? null,
}
: null,
bettingOpen: this.isMatchBettingOpen({
@@ -1327,29 +1296,49 @@ export class MatchesService {
}),
};
if (m.markets && !options?.omitMarkets) {
const csEnabled = m.correctScoreEnabled ?? true;
const CORRECT_SCORE_TYPES = ['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE'];
return {
...base,
markets: m.markets
.filter((market) => csEnabled || !CORRECT_SCORE_TYPES.includes(market.marketType as string))
.filter(
(market) =>
((market.showOnPlayer as boolean | undefined) ?? true) &&
isSettlementSupportedMarketType(market.marketType as string),
)
.map((market) => ({
id: (market.id as bigint).toString(),
marketType: market.marketType as string,
period: market.period as string,
status: (market.status as string) ?? 'OPEN',
lineValue: market.lineValue != null ? Number(market.lineValue) : null,
allowParlay: (market.allowParlay as boolean | undefined) ?? true,
promoLabel: (market.promoLabel as string | null | undefined) ?? null,
selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
id: (s.id as bigint).toString(),
selectionCode: s.selectionCode as string,
selectionName: s.selectionName as string,
status: (s.status as string) ?? 'OPEN',
odds: Number(s.odds),
oddsVersion: (s.oddsVersion as bigint).toString(),
id: (market.id as bigint).toString(),
marketType: market.marketType as string,
marketKey: (market.marketKey as string | null | undefined) ?? (market.marketType as string),
lineKey: (market.lineKey as string | null | undefined) ?? null,
period: market.period as string,
status: (market.status as string) ?? 'OPEN',
lineValue: market.lineValue != null ? Number(market.lineValue) : null,
allowSingle: (market.allowSingle as boolean | undefined) ?? true,
allowParlay: (market.allowParlay as boolean | undefined) ?? true,
marketDisplayName:
resolveMarketText(market.nameI18n, locale, defaultMarketName(market.marketType as string, locale)) ||
(market.marketType as string),
promoLabel:
resolveMarketText(
market.promoLabelI18n,
locale,
(market.promoLabel as string | null | undefined) ?? '',
) || null,
selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
id: (s.id as bigint).toString(),
selectionCode: s.selectionCode as string,
selectionName: s.selectionName as string,
selectionDisplayName:
resolveMarketText(
s.nameI18n,
locale,
defaultSelectionName(market.marketType as string, s.selectionCode as string, locale) ||
(s.selectionName as string),
) || (s.selectionName as string),
status: (s.status as string) ?? 'OPEN',
odds: Number(s.odds),
oddsVersion: (s.oddsVersion as bigint).toString(),
})),
})),
})),
};
}
return base;
@@ -1392,7 +1381,7 @@ export class MatchesService {
}
private playerMarketInclude = {
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] }, showOnPlayer: true },
include: {
selections: {
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
@@ -1404,7 +1393,7 @@ export class MatchesService {
/** 仅 status 字段,用于列表页计算 bettingOpen不返回给客户端 */
private playerMarketStatusInclude = {
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] }, showOnPlayer: true },
select: {
status: true,
selections: { select: { status: true } },
@@ -1421,6 +1410,7 @@ export class MatchesService {
const marketWhere = {
status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] },
showOnPlayer: true,
...(parlayMarketsOnly ? { marketType: { in: [...PLAYER_PARLAY_MARKET_TYPES] } } : {}),
};
@@ -1576,8 +1566,8 @@ export class MatchesService {
SH_CORRECT_SCORE: { 'zh-CN': '下半场波胆', 'en-US': '2H Correct Score', 'ms-MY': 'Skor Tepat PB2' },
};
const entry = labels[marketType];
if (!entry) return marketType;
return entry[locale] ?? entry['en-US'] ?? marketType;
if (!entry) return defaultMarketName(marketType, locale);
return entry[locale] ?? entry['en-US'] ?? defaultMarketName(marketType, locale);
}
async enrichBetsForHistory(
@@ -1594,6 +1584,7 @@ export class MatchesService {
selections: Array<{
matchId: bigint | null;
marketType: string;
marketNameSnapshot?: string | null;
selectionNameSnapshot: string;
odds: unknown;
resultStatus?: string | null;
@@ -1662,7 +1653,7 @@ export class MatchesService {
const m = mid ? matchMeta.get(mid) : undefined;
return {
marketType: sel.marketType,
marketLabel: this.marketLabelKey(sel.marketType, locale),
marketLabel: sel.marketNameSnapshot || this.marketLabelKey(sel.marketType, locale),
selectionName: sel.selectionNameSnapshot,
odds: sel.odds,
resultStatus: sel.resultStatus,

View File

@@ -92,19 +92,30 @@ export async function syncWc2026OutrightMarket(
startTime: new Date('2027-07-01T00:00:00Z'),
status: 'PUBLISHED',
publishTime: new Date(),
isHot: true,
isHot: false,
displayOrder: 0,
},
});
} else if (match.status === 'DRAFT' || match.status === 'SETTLED' || match.status === 'CLOSED') {
match = await prisma.match.update({
where: { id: match.id },
data: {
status: 'PUBLISHED',
publishTime: match.publishTime ?? new Date(),
closeTime: null,
},
});
} else {
const shouldPublish =
match.status === 'DRAFT' || match.status === 'SETTLED' || match.status === 'CLOSED';
if (match.isHot || shouldPublish) {
const updateData: {
isHot: boolean;
status?: string;
publishTime?: Date;
closeTime?: null;
} = { isHot: false };
if (shouldPublish) {
updateData.status = 'PUBLISHED';
updateData.publishTime = match.publishTime ?? new Date();
updateData.closeTime = null;
}
match = await prisma.match.update({
where: { id: match.id },
data: updateData,
});
}
}
let market = await prisma.market.findFirst({