Files
thebet365/apps/api/src/domains/catalog/wc2026-outright.sync.ts
2026-06-13 17:38:25 +08:00

195 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}