195 lines
5.2 KiB
TypeScript
195 lines
5.2 KiB
TypeScript
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 };
|
||
}
|