import type { PrismaClient } from '@prisma/client'; import { Decimal } from '@prisma/client/runtime/library'; import { WC2026_LEAGUE_CODE, WC2026_OUTRIGHT_TEAMS, type Wc2026OutrightTeam, } from './wc2026-outright-teams'; const PLACEHOLDER_TEAM_CODE = 'OUT'; const CANONICAL_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code)); export type Wc2026OutrightSyncOptions = { /** true:赔率/队名/排序与 wc2026-outright-teams.ts 完全一致,并关闭不在表内的选项 */ forceCanonical?: boolean; }; function oddsEqual(a: Decimal | number, b: number) { return Number(a) === b; } async function upsertTeamTranslations( prisma: PrismaClient, teamId: bigint, names: Wc2026OutrightTeam['names'], ) { for (const [locale, value] of Object.entries(names)) { await 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 }, }); } } async function upsertTeam(prisma: PrismaClient, entry: Wc2026OutrightTeam) { const team = await prisma.team.upsert({ where: { code: entry.code }, create: { code: entry.code }, update: {}, }); await upsertTeamTranslations(prisma, team.id, entry.names); return team; } /** 确保 WC2026 夺冠盘存在;forceCanonical 时与基准表完全一致 */ export async function syncWc2026OutrightMarket( prisma: PrismaClient, options: Wc2026OutrightSyncOptions = {}, ) { const forceCanonical = options.forceCanonical ?? false; const league = await prisma.league.findUnique({ where: { code: WC2026_LEAGUE_CODE } }); if (!league) { throw new Error(`League ${WC2026_LEAGUE_CODE} not found — run seedCatalog first`); } const placeholder = await upsertTeam(prisma, { rank: 0, code: PLACEHOLDER_TEAM_CODE, names: { 'zh-CN': '冠军盘', 'en-US': 'Outright' }, defaultOdds: 1, }); for (const entry of WC2026_OUTRIGHT_TEAMS) { await upsertTeam(prisma, entry); } let match = await prisma.match.findFirst({ where: { leagueId: league.id, isOutright: true, deletedAt: null }, }); if (!match) { match = await prisma.match.create({ data: { leagueId: league.id, homeTeamId: placeholder.id, awayTeamId: placeholder.id, isOutright: true, matchName: '2026 FIFA World Cup Winner', startTime: new Date('2027-07-01T00:00:00Z'), status: 'PUBLISHED', publishTime: new Date(), isHot: false, displayOrder: 0, }, }); } 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({ where: { matchId: match.id, marketType: 'OUTRIGHT_WINNER' }, include: { selections: true }, }); if (!market) { market = await prisma.market.create({ data: { matchId: match.id, marketType: 'OUTRIGHT_WINNER', period: 'OUTRIGHT', allowSingle: true, allowParlay: false, sortOrder: 1, status: 'OPEN', }, include: { selections: true }, }); } const existingByCode = new Map(market.selections.map((s) => [s.selectionCode, s])); for (const entry of WC2026_OUTRIGHT_TEAMS) { const sortOrder = entry.rank - 1; const existing = existingByCode.get(entry.code); if (!existing) { await prisma.marketSelection.create({ data: { marketId: market.id, selectionCode: entry.code, selectionName: entry.names['zh-CN'], odds: entry.defaultOdds, sortOrder, status: 'OPEN', }, }); continue; } const updateData: { selectionName: string; sortOrder: number; status: string; odds?: number; oddsVersion?: bigint; } = { selectionName: entry.names['zh-CN'], sortOrder, status: 'OPEN', }; if (forceCanonical && !oddsEqual(existing.odds, entry.defaultOdds)) { updateData.odds = entry.defaultOdds; updateData.oddsVersion = existing.oddsVersion + BigInt(1); } await prisma.marketSelection.update({ where: { id: existing.id }, data: updateData, }); } if (forceCanonical) { for (const sel of market.selections) { if (CANONICAL_CODES.has(sel.selectionCode)) continue; if (sel.selectionCode === PLACEHOLDER_TEAM_CODE) continue; await prisma.marketSelection.update({ where: { id: sel.id }, data: { status: 'CLOSED' }, }); } } return { matchId: match.id, marketId: market.id }; }