feat(admin,player,api): 优胜冠军通用管理与界面精简

管理端新增冠军盘列表/编辑、展开懒加载与 ECharts 修复;各列表页去掉重复标题。玩家端支持多赛事冠军盘、分批加载与语言切换刷新。API 扩展 outright CRUD 与列表性能优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 09:17:01 +08:00
parent 9b63d67e7c
commit 27580b2479
39 changed files with 2250 additions and 578 deletions

View File

@@ -260,6 +260,52 @@ class BatchOutrightOddsDto {
updates!: OutrightOddsUpdateItemDto[];
}
class CreateOutrightDto {
@IsString()
leagueId!: string;
@IsString()
titleZh!: string;
@IsString()
titleEn!: string;
@IsOptional()
@IsString()
status?: string;
}
class UpdateOutrightDto {
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
matchName?: string;
@IsOptional()
isHot?: boolean;
@IsOptional()
displayOrder?: number;
}
class AddOutrightSelectionDto {
@IsString()
teamCode!: string;
@IsString()
teamZh!: string;
@IsString()
teamEn!: string;
@IsNumber()
@Min(1.01)
odds!: number;
}
class CashbackPreviewDto {
@IsString()
periodStart!: string;
@@ -648,24 +694,111 @@ export class AdminController {
return jsonResponse(selection);
}
@Get('outrights/wc2026')
async getWc2026Outright() {
const data = await this.outright.getWc2026ForAdmin();
@Get('outrights')
async listOutrights() {
const data = await this.outright.listForAdmin();
return jsonResponse(data);
}
@Get('outrights/leagues')
async listOutrightLeagues() {
const data = await this.outright.listLeagueOptions();
return jsonResponse(data);
}
@Post('outrights')
async createOutright(@Body() dto: CreateOutrightDto) {
const data = await this.outright.createForAdmin({
leagueId: BigInt(dto.leagueId),
titleZh: dto.titleZh,
titleEn: dto.titleEn,
status: dto.status,
});
return jsonResponse(data);
}
@Post('outrights/import/wc2026')
async importWc2026Outright() {
const data = await this.outright.importWc2026Canonical();
return jsonResponse(data);
}
/** @deprecated */
@Get('outrights/wc2026')
async getWc2026OutrightLegacy() {
const list = await this.outright.listForAdmin();
const wc = list.find((e) => e.leagueCode === 'WC2026');
if (!wc) throw new BadRequestException('WC2026 outright not found — run import');
return jsonResponse(await this.outright.getForAdmin(BigInt(wc.id)));
}
/** @deprecated */
@Put('outrights/wc2026/odds')
async updateWc2026OutrightOdds(
async updateWc2026OutrightOddsLegacy(
@CurrentUser('id') operatorId: bigint,
@Body() dto: BatchOutrightOddsDto,
) {
const data = await this.outright.updateWc2026Odds(dto.updates, operatorId);
const list = await this.outright.listForAdmin();
const wc = list.find((e) => e.leagueCode === 'WC2026');
if (!wc) throw new BadRequestException('WC2026 outright not found');
return jsonResponse(
await this.outright.batchUpdateOdds(BigInt(wc.id), dto.updates, operatorId),
);
}
/** @deprecated */
@Post('outrights/wc2026/apply-canonical')
async applyWc2026CanonicalLegacy() {
return jsonResponse(await this.outright.importWc2026Canonical());
}
@Get('outrights/:matchId')
async getOutright(@Param('matchId') matchId: string) {
const data = await this.outright.getForAdmin(BigInt(matchId));
return jsonResponse(data);
}
@Post('outrights/wc2026/apply-canonical')
async applyWc2026Canonical() {
const data = await this.outright.applyWc2026Canonical();
@Put('outrights/:matchId')
async updateOutright(
@Param('matchId') matchId: string,
@Body() dto: UpdateOutrightDto,
) {
const data = await this.outright.updateForAdmin(BigInt(matchId), dto);
return jsonResponse(data);
}
@Put('outrights/:matchId/odds')
async updateOutrightOdds(
@CurrentUser('id') operatorId: bigint,
@Param('matchId') matchId: string,
@Body() dto: BatchOutrightOddsDto,
) {
const data = await this.outright.batchUpdateOdds(
BigInt(matchId),
dto.updates,
operatorId,
);
return jsonResponse(data);
}
@Post('outrights/:matchId/selections')
async addOutrightSelection(
@Param('matchId') matchId: string,
@Body() dto: AddOutrightSelectionDto,
) {
const data = await this.outright.addSelection(BigInt(matchId), dto);
return jsonResponse(data);
}
@Delete('outrights/:matchId/selections/:selectionId')
async removeOutrightSelection(
@Param('matchId') matchId: string,
@Param('selectionId') selectionId: string,
) {
const data = await this.outright.closeSelection(
BigInt(matchId),
BigInt(selectionId),
);
return jsonResponse(data);
}

View File

@@ -15,6 +15,7 @@ import { jsonResponse } from '../../shared/common/filters';
import { UsersService } from '../../domains/identity/users.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { MatchesService } from '../../domains/catalog/matches.service';
import { OutrightService } from '../../domains/catalog/outright.service';
import { BetsService } from '../../domains/betting/bets.service';
import { ContentService } from '../../domains/operations/content/content.service';
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
@@ -82,6 +83,7 @@ export class PlayerController {
private users: UsersService,
private wallet: WalletService,
private matches: MatchesService,
private outright: OutrightService,
private bets: BetsService,
private content: ContentService,
private cashback: CashbackService,
@@ -134,7 +136,7 @@ export class PlayerController {
@Get('outrights')
async listOutrights(@CurrentUser('locale') locale: string) {
const items = await this.matches.listOutrights(locale);
const items = await this.outright.listForPlayer(locale);
return jsonResponse(items);
}

View File

@@ -14,12 +14,9 @@ import {
toVenueJson,
translationsFromZhiboNames,
} from './zhibo-match.mapper';
import { WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
const WC2026_OUTRIGHT_RANK = new Map(
WC2026_OUTRIGHT_TEAMS.map((t) => [t.code, t.rank]),
);
const WC2026_OUTRIGHT_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code));
const OUTRIGHT_PLACEHOLDER_CODE = 'OUT';
@Injectable()
export class MatchesService {
@@ -570,8 +567,19 @@ export class MatchesService {
}
async listOutrights(locale = 'en-US') {
try {
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: false });
} catch {
/* 联赛未 seed 时忽略,仍返回已有数据 */
}
const matches = await this.prisma.match.findMany({
where: { status: 'PUBLISHED', isOutright: true, sportType: 'FOOTBALL' },
where: {
status: 'PUBLISHED',
isOutright: true,
sportType: 'FOOTBALL',
deletedAt: null,
},
include: {
markets: {
where: { marketType: 'OUTRIGHT_WINNER', status: 'OPEN' },
@@ -592,27 +600,28 @@ export class MatchesService {
const market = match.markets[0];
if (!market) continue;
const selections = (
await Promise.all(
market.selections
.filter((sel) => WC2026_OUTRIGHT_CODES.has(sel.selectionCode))
.map(async (sel) => {
const teamCode = sel.selectionCode.replace(/^TEAM_/, '');
const team = await this.prisma.team.findUnique({ where: { code: teamCode } });
const teamName = team
? await this.getTranslation('TEAM', team.id, locale)
: sel.selectionName;
return {
id: sel.id.toString(),
teamCode,
teamName,
rank: WC2026_OUTRIGHT_RANK.get(teamCode) ?? 999,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};
}),
)
).sort((a, b) => a.rank - b.rank);
const selections = await Promise.all(
market.selections
.filter((sel) => sel.selectionCode !== OUTRIGHT_PLACEHOLDER_CODE)
.map(async (sel) => {
const team = await this.prisma.team.findUnique({
where: { code: sel.selectionCode },
});
const teamName = team
? await this.getTranslation('TEAM', team.id, locale)
: sel.selectionName;
return {
id: sel.id.toString(),
teamCode: sel.selectionCode,
teamName,
rank: sel.sortOrder + 1,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};
}),
);
if (selections.length === 0) continue;
results.push({
id: match.id.toString(),

View File

@@ -1,10 +1,15 @@
import { Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { MarketsService } from '../odds/markets.service';
import { WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
import { WC2026_LEAGUE_CODE, WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
const CANONICAL_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code));
const PLACEHOLDER_TEAM_CODE = 'OUT';
const OUTRIGHT_MARKET_TYPE = 'OUTRIGHT_WINNER';
@Injectable()
export class OutrightService {
@@ -13,86 +18,518 @@ export class OutrightService {
private markets: MarketsService,
) {}
async getWc2026ForAdmin() {
const { matchId, marketId } = await syncWc2026OutrightMarket(this.prisma, {
forceCanonical: false,
async listForAdmin() {
const matches = await this.prisma.match.findMany({
where: { isOutright: true, sportType: 'FOOTBALL', deletedAt: null },
include: {
league: true,
markets: {
where: { marketType: OUTRIGHT_MARKET_TYPE },
select: {
id: true,
status: true,
_count: {
select: {
selections: {
where: {
status: { not: 'CLOSED' },
selectionCode: { not: PLACEHOLDER_TEAM_CODE },
},
},
},
},
selections: {
where: {
status: 'OPEN',
selectionCode: { not: PLACEHOLDER_TEAM_CODE },
},
select: { id: true },
take: 1,
},
},
},
},
orderBy: [{ displayOrder: 'asc' }, { startTime: 'asc' }],
});
const match = await this.prisma.match.findUniqueOrThrow({ where: { id: matchId } });
const [leagueZh, leagueEn, market] = await Promise.all([
this.getTranslation('LEAGUE', match.leagueId, 'zh-CN'),
this.getTranslation('LEAGUE', match.leagueId, 'en-US'),
this.prisma.market.findUniqueOrThrow({
where: { id: marketId },
include: {
selections: { orderBy: { sortOrder: 'asc' } },
},
}),
]);
const teamByCode = new Map(
WC2026_OUTRIGHT_TEAMS.map((t) => [t.code, t]),
);
const canonicalSelections = market.selections.filter((sel) =>
CANONICAL_CODES.has(sel.selectionCode),
);
const selections = await Promise.all(
canonicalSelections.map(async (sel) => {
const meta = teamByCode.get(sel.selectionCode);
const team = await this.prisma.team.findUnique({ where: { code: sel.selectionCode } });
const teamZh = team
? await this.getTranslation('TEAM', team.id, 'zh-CN')
: sel.selectionName;
const teamEn = team
? await this.getTranslation('TEAM', team.id, 'en-US')
: sel.selectionName;
return Promise.all(
matches.map(async (m) => {
const league = m.league;
const leagueCode = league.code;
const [leagueZh, leagueEn] = await Promise.all([
this.getTranslation('LEAGUE', league.id, 'zh-CN'),
this.getTranslation('LEAGUE', league.id, 'en-US'),
]);
const market = m.markets[0];
const openCount = market?.selections.length ?? 0;
const selectionCount = market?._count.selections ?? 0;
const visibility = this.playerVisibilityByCounts(
m.status,
market,
openCount,
);
return {
id: sel.id.toString(),
teamCode: sel.selectionCode,
rank: meta?.rank ?? sel.sortOrder + 1,
teamZh,
teamEn,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
status: sel.status,
id: m.id.toString(),
leagueId: league.id.toString(),
leagueCode,
leagueZh,
leagueEn,
matchName: m.matchName ?? '',
status: m.status,
selectionCount,
canImportCanonical: leagueCode === WC2026_LEAGUE_CODE,
playerVisible: visibility.playerVisible,
playerHiddenReason: visibility.playerHiddenReason,
};
}),
);
}
selections.sort((a, b) => a.rank - b.rank);
async listLeagueOptions() {
const leagues = await this.prisma.league.findMany({
where: { isActive: true, deletedAt: null, sportType: 'FOOTBALL' },
orderBy: { displayOrder: 'asc' },
});
return Promise.all(
leagues.map(async (l) => ({
id: l.id.toString(),
code: l.code,
nameZh: await this.getTranslation('LEAGUE', l.id, 'zh-CN'),
nameEn: await this.getTranslation('LEAGUE', l.id, 'en-US'),
})),
);
}
async getForAdmin(matchId: bigint) {
const match = await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(match.id);
const league = await this.prisma.league.findUniqueOrThrow({
where: { id: match.leagueId },
});
const [leagueZh, leagueEn] = await Promise.all([
this.getTranslation('LEAGUE', match.leagueId, 'zh-CN'),
this.getTranslation('LEAGUE', match.leagueId, 'en-US'),
]);
const fullMarket = await this.prisma.market.findUniqueOrThrow({
where: { id: market.id },
include: {
selections: { orderBy: { sortOrder: 'asc' } },
},
});
const selections = await Promise.all(
fullMarket.selections
.filter((s) => s.selectionCode !== PLACEHOLDER_TEAM_CODE)
.map(async (sel, index) => {
const team = await this.prisma.team.findUnique({
where: { code: sel.selectionCode },
});
const teamZh = team
? await this.getTranslation('TEAM', team.id, 'zh-CN')
: sel.selectionName;
const teamEn = team
? await this.getTranslation('TEAM', team.id, 'en-US')
: sel.selectionName;
return {
id: sel.id.toString(),
teamCode: sel.selectionCode,
rank: sel.sortOrder + 1 || index + 1,
teamZh: teamZh || sel.selectionName,
teamEn: teamEn || sel.selectionName,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
status: sel.status,
};
}),
);
const visibility = this.playerVisibility(
match.status,
fullMarket,
fullMarket.selections.filter(
(s) => s.selectionCode !== PLACEHOLDER_TEAM_CODE,
),
);
return {
matchId: matchId.toString(),
marketId: marketId.toString(),
matchStatus: match.status,
marketStatus: market.status,
id: match.id.toString(),
leagueId: match.leagueId.toString(),
leagueCode: league.code,
leagueZh,
leagueEn,
matchName: match.matchName ?? '',
status: match.status,
marketId: fullMarket.id.toString(),
marketStatus: fullMarket.status,
canImportCanonical: league.code === WC2026_LEAGUE_CODE,
expectedCanonicalCount:
league.code === WC2026_LEAGUE_CODE ? WC2026_OUTRIGHT_TEAMS.length : null,
playerVisible: visibility.playerVisible,
playerHiddenReason: visibility.playerHiddenReason,
selections,
expectedCount: WC2026_OUTRIGHT_TEAMS.length,
};
}
async applyWc2026Canonical() {
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: true });
return this.getWc2026ForAdmin();
async createForAdmin(data: {
leagueId: bigint;
titleZh: string;
titleEn: string;
status?: string;
isHot?: boolean;
displayOrder?: number;
startTime?: Date;
}) {
const league = await this.prisma.league.findUnique({
where: { id: data.leagueId },
});
if (!league) throw new NotFoundException('League not found');
const placeholder = await this.ensurePlaceholderTeam();
const status = data.status ?? 'PUBLISHED';
const match = await this.prisma.match.create({
data: {
leagueId: data.leagueId,
homeTeamId: placeholder.id,
awayTeamId: placeholder.id,
isOutright: true,
matchName: data.titleEn || data.titleZh,
startTime: data.startTime ?? new Date('2030-01-01T00:00:00Z'),
status,
publishTime: status === 'PUBLISHED' ? new Date() : undefined,
isHot: data.isHot ?? false,
displayOrder: data.displayOrder ?? 0,
},
});
await this.ensureOutrightMarket(match.id);
return this.getForAdmin(match.id);
}
async updateWc2026Odds(
async updateForAdmin(
matchId: bigint,
data: {
status?: string;
matchName?: string;
isHot?: boolean;
displayOrder?: number;
titleZh?: string;
titleEn?: string;
},
) {
const match = await this.getOutrightMatchOrThrow(matchId);
const status = data.status ?? match.status;
await this.prisma.match.update({
where: { id: matchId },
data: {
status,
matchName: data.matchName,
isHot: data.isHot,
displayOrder: data.displayOrder,
publishTime:
status === 'PUBLISHED' && !match.publishTime
? new Date()
: match.publishTime,
},
});
return this.getForAdmin(matchId);
}
async addSelection(
matchId: bigint,
data: {
teamCode: string;
teamZh: string;
teamEn: string;
odds: number;
},
) {
if (!data.teamCode?.trim()) {
throw new BadRequestException('Team code required');
}
if (data.odds <= 1) {
throw new BadRequestException('Odds must be greater than 1');
}
const match = await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(match.id);
const code = data.teamCode.trim().toUpperCase();
const team = await this.prisma.team.upsert({
where: { code },
create: { code },
update: {},
});
await this.upsertTeamTranslations(team.id, {
'zh-CN': data.teamZh.trim() || data.teamEn,
'en-US': data.teamEn.trim() || data.teamZh,
});
const existing = await this.prisma.marketSelection.findFirst({
where: { marketId: market.id, selectionCode: code },
});
if (existing) {
throw new BadRequestException('Selection already exists for this team code');
}
const maxSort = await this.prisma.marketSelection.aggregate({
where: { marketId: market.id },
_max: { sortOrder: true },
});
await this.prisma.marketSelection.create({
data: {
marketId: market.id,
selectionCode: code,
selectionName: data.teamZh.trim() || data.teamEn,
odds: data.odds,
sortOrder: (maxSort._max.sortOrder ?? -1) + 1,
status: 'OPEN',
},
});
return this.getForAdmin(matchId);
}
async closeSelection(matchId: bigint, selectionId: bigint) {
const match = await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(match.id);
const sel = await this.prisma.marketSelection.findFirst({
where: { id: selectionId, marketId: market.id },
});
if (!sel) throw new NotFoundException('Selection not found');
await this.prisma.marketSelection.update({
where: { id: selectionId },
data: { status: 'CLOSED' },
});
return this.getForAdmin(matchId);
}
async batchUpdateOdds(
matchId: bigint,
updates: Array<{ selectionId: string; odds: number }>,
operatorId: bigint,
) {
const results = await this.markets.batchUpdateOdds(
await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(matchId);
const allowed = new Set(
(
await this.prisma.marketSelection.findMany({
where: { marketId: market.id },
select: { id: true },
})
).map((s) => s.id.toString()),
);
for (const u of updates) {
if (!allowed.has(u.selectionId)) {
throw new BadRequestException('Invalid selection for this outright event');
}
}
await this.markets.batchUpdateOdds(
updates.map((u) => ({ selectionId: BigInt(u.selectionId), odds: u.odds })),
operatorId,
);
return results.map((r) => ({
id: r.id.toString(),
odds: r.odds.toString(),
oddsVersion: r.oddsVersion.toString(),
}));
return this.getForAdmin(matchId);
}
async importWc2026Canonical() {
const { matchId } = await syncWc2026OutrightMarket(this.prisma, {
forceCanonical: true,
});
return this.getForAdmin(matchId);
}
async listForPlayer(locale: string) {
await this.trySyncWc2026();
const matches = await this.prisma.match.findMany({
where: {
status: 'PUBLISHED',
isOutright: true,
sportType: 'FOOTBALL',
deletedAt: null,
},
include: {
markets: {
where: { marketType: OUTRIGHT_MARKET_TYPE, status: 'OPEN' },
include: {
selections: {
where: { status: 'OPEN' },
orderBy: { sortOrder: 'asc' },
},
},
},
},
orderBy: [{ displayOrder: 'asc' }, { startTime: 'asc' }],
});
const results = [];
for (const match of matches) {
const league = await this.prisma.league.findUniqueOrThrow({
where: { id: match.leagueId },
});
const leagueName = await this.getTranslation('LEAGUE', match.leagueId, locale);
const market = match.markets[0];
if (!market) continue;
const selections = await Promise.all(
market.selections
.filter((sel) => sel.selectionCode !== PLACEHOLDER_TEAM_CODE)
.map(async (sel) => {
const team = await this.prisma.team.findUnique({
where: { code: sel.selectionCode },
});
const translated = team
? await this.getTranslation('TEAM', team.id, locale)
: '';
const teamZh = team
? await this.getTranslation('TEAM', team.id, 'zh-CN')
: sel.selectionName;
const teamEn = team
? await this.getTranslation('TEAM', team.id, 'en-US')
: sel.selectionName;
const teamName =
translated ||
(locale.startsWith('zh') ? teamZh : teamEn) ||
sel.selectionName;
return {
id: sel.id.toString(),
teamCode: sel.selectionCode,
teamName,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};
}),
);
if (!selections.length) continue;
const title =
match.matchName?.trim() ||
`*${leagueName || 'Outright'} ${locale.startsWith('zh') ? '冠军' : 'Winner'}`;
results.push({
id: match.id.toString(),
leagueId: match.leagueId.toString(),
leagueCode: league.code,
leagueName: leagueName || '',
title: title.startsWith('*') ? title : `*${title}`,
marketId: market.id.toString(),
selectionCount: selections.length,
selections,
});
}
return results;
}
/** @deprecated 使用 listForPlayer */
async getWc2026ForPlayer(locale: string) {
return this.listForPlayer(locale);
}
private async trySyncWc2026() {
try {
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: false });
} catch {
/* 联赛未 seed 时忽略 */
}
}
private async getOutrightMatchOrThrow(matchId: bigint) {
const match = await this.prisma.match.findFirst({
where: { id: matchId, isOutright: true, deletedAt: null },
});
if (!match) throw new NotFoundException('Outright event not found');
return match;
}
private async ensureOutrightMarket(matchId: bigint) {
let market = await this.prisma.market.findFirst({
where: { matchId, marketType: OUTRIGHT_MARKET_TYPE },
});
if (!market) {
market = await this.prisma.market.create({
data: {
matchId,
marketType: OUTRIGHT_MARKET_TYPE,
period: 'OUTRIGHT',
allowSingle: true,
allowParlay: false,
sortOrder: 1,
status: 'OPEN',
},
});
}
return market;
}
private async ensurePlaceholderTeam() {
const existing = await this.prisma.team.findUnique({
where: { code: PLACEHOLDER_TEAM_CODE },
});
if (existing) return existing;
return this.prisma.team.create({
data: { code: PLACEHOLDER_TEAM_CODE },
});
}
private async upsertTeamTranslations(
teamId: bigint,
names: Record<string, string>,
) {
for (const [locale, value] of Object.entries(names)) {
if (!value) continue;
await this.prisma.entityTranslation.upsert({
where: {
entityType_entityId_locale_fieldName: {
entityType: 'TEAM',
entityId: teamId,
locale,
fieldName: 'name',
},
},
create: {
entityType: 'TEAM',
entityId: teamId,
locale,
fieldName: 'name',
value,
},
update: { value },
});
}
}
private playerVisibility(
matchStatus: string,
market: { status: string } | null | undefined,
selections: Array<{ selectionCode: string; status: string }>,
): { playerVisible: boolean; playerHiddenReason: string | null } {
const openCount = selections.filter(
(s) => s.status === 'OPEN' && s.selectionCode !== PLACEHOLDER_TEAM_CODE,
).length;
return this.playerVisibilityByCounts(matchStatus, market, openCount);
}
private playerVisibilityByCounts(
matchStatus: string,
market: { status: string } | null | undefined,
openSelectionCount: number,
): { playerVisible: boolean; playerHiddenReason: string | null } {
if (matchStatus !== 'PUBLISHED') {
return { playerVisible: false, playerHiddenReason: 'NOT_PUBLISHED' };
}
if (!market || market.status !== 'OPEN') {
return { playerVisible: false, playerHiddenReason: 'MARKET_CLOSED' };
}
if (openSelectionCount === 0) {
return { playerVisible: false, playerHiddenReason: 'NO_SELECTIONS' };
}
return { playerVisible: true, playerHiddenReason: null };
}
private async getTranslation(