Files
thebet365/packages/shared/src/market-catalog.ts
2026-06-13 17:38:25 +08:00

635 lines
21 KiB
TypeScript

import { resolveTranslationFallback } from './locale';
export const MARKET_LOCALES = ['zh-CN', 'en-US', 'ms-MY'] as const;
export type MarketLocale = (typeof MARKET_LOCALES)[number];
export type LocalizedText = Partial<Record<MarketLocale, string>>;
export type FootballMarketPeriod = 'FT' | 'HT' | 'SH' | 'OUTRIGHT';
export type MarketRenderType = 'standard' | 'correctScore' | 'unsupported';
export type MarketSettlementKind =
| 'SCORE'
| 'HANDICAP'
| 'TOTAL'
| 'ODD_EVEN'
| 'CORRECT_SCORE'
| 'OUTRIGHT'
| 'MANUAL_STATS'
| 'UNSUPPORTED';
export type MarketSelectionTemplate = {
code: string;
name: string;
nameI18n: LocalizedText;
odds: number;
};
export type FootballMarketCatalogEntry = {
marketKey: string;
period: FootballMarketPeriod;
sortOrder: number;
defaultLineValue: number | null;
defaultParams?: Record<string, string | number | boolean | null>;
allowSingle: boolean;
allowParlay: boolean;
showOnPlayer: boolean;
settlementSupported: boolean;
settlementKind: MarketSettlementKind;
renderType: MarketRenderType;
detailOrder: number | null;
parlayOrder: number | null;
i18nKey: string;
adminI18nKey: string;
nameI18n: LocalizedText;
selectionTemplate: readonly MarketSelectionTemplate[];
};
export const FT_CORRECT_SCORE_TEMPLATE = [
'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'SCORE_3_3', 'SCORE_4_4', 'OTHER_DRAW',
'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'SCORE_3_1', 'SCORE_3_2',
'SCORE_4_0', 'SCORE_4_1', 'SCORE_4_2', 'SCORE_4_3', 'OTHER_HOME',
'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'SCORE_1_3', 'SCORE_2_3',
'SCORE_0_4', 'SCORE_1_4', 'SCORE_2_4', 'SCORE_3_4', 'OTHER_AWAY',
] as const;
export const HT_CORRECT_SCORE_TEMPLATE = [
'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'OTHER_DRAW',
'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'OTHER_HOME',
'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'OTHER_AWAY',
] as const;
function text(zh: string, en: string, ms: string): LocalizedText {
return { 'zh-CN': zh, 'en-US': en, 'ms-MY': ms };
}
function selection(code: string, zh: string, en: string, ms: string, odds: number): MarketSelectionTemplate {
const nameI18n = text(zh, en, ms);
return { code, name: zh, nameI18n, odds };
}
function handicapName(side: 'home' | 'away', line: number, half = false) {
const sideLabel = side === 'home' ? '主队' : '客队';
const value = side === 'home' ? line : -line;
const lineText = value > 0 ? `+${value}` : `${value}`;
return half ? `半场${sideLabel} ${lineText}` : `${sideLabel} ${lineText}`;
}
function totalName(side: 'over' | 'under', line: number, half = false) {
const sideLabel = side === 'over' ? '大' : '小';
return half ? `半场${sideLabel} ${line}` : `${sideLabel} ${line}`;
}
function scoreName(code: string) {
if (code === 'OTHER_HOME') return '其它主胜';
if (code === 'OTHER_DRAW') return '其它和局';
if (code === 'OTHER_AWAY') return '其它客胜';
return code.replace('SCORE_', '').replace('_', '-');
}
function scoreSelection(code: string, odds: number): MarketSelectionTemplate {
const display = scoreName(code);
const en =
code === 'OTHER_HOME'
? 'Any Other Home Win'
: code === 'OTHER_DRAW'
? 'Any Other Draw'
: code === 'OTHER_AWAY'
? 'Any Other Away Win'
: display;
const ms =
code === 'OTHER_HOME'
? 'Lain-lain Menang Tuan Rumah'
: code === 'OTHER_DRAW'
? 'Lain-lain Seri'
: code === 'OTHER_AWAY'
? 'Lain-lain Menang Pelawat'
: display;
return selection(code, display, en, ms, odds);
}
const ftCorrectScoreSelections = FT_CORRECT_SCORE_TEMPLATE.map((code) => scoreSelection(code, 8));
const htCorrectScoreSelections = HT_CORRECT_SCORE_TEMPLATE.map((code) => scoreSelection(code, 6));
const teamTotalSelections = [
selection('OVER', totalName('over', 1.5), 'Over', 'Atas', 1.85),
selection('UNDER', totalName('under', 1.5), 'Under', 'Bawah', 1.95),
] as const;
const htFtSelections = [
selection('HOME_HOME', '主/主', 'Home/Home', 'Tuan Rumah/Tuan Rumah', 2.9),
selection('HOME_DRAW', '主/和', 'Home/Draw', 'Tuan Rumah/Seri', 15),
selection('HOME_AWAY', '主/客', 'Home/Away', 'Tuan Rumah/Pelawat', 26),
selection('DRAW_HOME', '和/主', 'Draw/Home', 'Seri/Tuan Rumah', 4.8),
selection('DRAW_DRAW', '和/和', 'Draw/Draw', 'Seri/Seri', 4.5),
selection('DRAW_AWAY', '和/客', 'Draw/Away', 'Seri/Pelawat', 6.8),
selection('AWAY_HOME', '客/主', 'Away/Home', 'Pelawat/Tuan Rumah', 21),
selection('AWAY_DRAW', '客/和', 'Away/Draw', 'Pelawat/Seri', 15),
selection('AWAY_AWAY', '客/客', 'Away/Away', 'Pelawat/Pelawat', 4.6),
] as const;
const totalGoalsSelections = [
selection('TG_0_1', '0-1', '0-1', '0-1', 3.8),
selection('TG_2_3', '2-3', '2-3', '2-3', 2.2),
selection('TG_4_6', '4-6', '4-6', '4-6', 2.8),
selection('TG_7_PLUS', '7+', '7+', '7+', 12),
] as const;
const cornersHandicapSelections = [
selection('HOME', '主队角球', 'Home Corners', 'Sudut Tuan Rumah', 1.9),
selection('AWAY', '客队角球', 'Away Corners', 'Sudut Pelawat', 1.9),
] as const;
const cornersTotalSelections = [
selection('OVER', '大 8.5', 'Over', 'Atas', 1.85),
selection('UNDER', '小 8.5', 'Under', 'Bawah', 1.95),
] as const;
const cardsTotalSelections = [
selection('OVER', '大 3.5', 'Over', 'Atas', 1.85),
selection('UNDER', '小 3.5', 'Under', 'Bawah', 1.95),
] as const;
const unsupported = {
allowSingle: true,
allowParlay: false,
showOnPlayer: false,
settlementSupported: false,
settlementKind: 'UNSUPPORTED' as const,
renderType: 'unsupported' as const,
detailOrder: null,
parlayOrder: null,
selectionTemplate: [] as readonly MarketSelectionTemplate[],
};
export const FOOTBALL_MARKET_CATALOG = {
FT_1X2: {
marketKey: 'FT_1X2',
period: 'FT',
sortOrder: 1,
defaultLineValue: null,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'SCORE',
renderType: 'standard',
detailOrder: 5,
parlayOrder: 3,
i18nKey: 'bet.market_ft_1x2',
adminI18nKey: 'matchEditor.market.FT_1X2',
nameI18n: text('全场 1X2', 'FT 1X2', '1X2 Penuh'),
selectionTemplate: [
selection('HOME', '主', 'Home', 'Tuan Rumah', 2.5),
selection('DRAW', '和', 'Draw', 'Seri', 3.2),
selection('AWAY', '客', 'Away', 'Pelawat', 2.8),
],
},
FT_HANDICAP: {
marketKey: 'FT_HANDICAP',
period: 'FT',
sortOrder: 2,
defaultLineValue: -0.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'HANDICAP',
renderType: 'standard',
detailOrder: 3,
parlayOrder: 1,
i18nKey: 'bet.market_ft_handicap',
adminI18nKey: 'matchEditor.market.FT_HANDICAP',
nameI18n: text('全场让球', 'FT Handicap', 'Handicap Penuh'),
selectionTemplate: [
selection('HOME', handicapName('home', -0.5), 'Home', 'Tuan Rumah', 1.9),
selection('AWAY', handicapName('away', -0.5), 'Away', 'Pelawat', 1.9),
],
},
FT_OVER_UNDER: {
marketKey: 'FT_OVER_UNDER',
period: 'FT',
sortOrder: 3,
defaultLineValue: 2.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'TOTAL',
renderType: 'standard',
detailOrder: 4,
parlayOrder: 2,
i18nKey: 'bet.market_ft_ou',
adminI18nKey: 'matchEditor.market.FT_OVER_UNDER',
nameI18n: text('全场大小', 'FT Over/Under', 'Atas/Bawah Penuh'),
selectionTemplate: [
selection('OVER', totalName('over', 2.5), 'Over', 'Atas', 1.85),
selection('UNDER', totalName('under', 2.5), 'Under', 'Bawah', 1.95),
],
},
FT_ODD_EVEN: {
marketKey: 'FT_ODD_EVEN',
period: 'FT',
sortOrder: 4,
defaultLineValue: null,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'ODD_EVEN',
renderType: 'standard',
detailOrder: 6,
parlayOrder: 4,
i18nKey: 'bet.market_ft_oe',
adminI18nKey: 'matchEditor.market.FT_ODD_EVEN',
nameI18n: text('全场单双', 'FT Odd/Even', 'Ganjil/Genap Penuh'),
selectionTemplate: [
selection('ODD', '单', 'Odd', 'Ganjil', 1.9),
selection('EVEN', '双', 'Even', 'Genap', 1.9),
],
},
HT_1X2: {
marketKey: 'HT_1X2',
period: 'HT',
sortOrder: 5,
defaultLineValue: null,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'SCORE',
renderType: 'standard',
detailOrder: 9,
parlayOrder: 7,
i18nKey: 'bet.market_ht_1x2',
adminI18nKey: 'matchEditor.market.HT_1X2',
nameI18n: text('半场 1X2', 'HT 1X2', '1X2 Separuh'),
selectionTemplate: [
selection('HOME', '主', 'Home', 'Tuan Rumah', 3),
selection('DRAW', '和', 'Draw', 'Seri', 2),
selection('AWAY', '客', 'Away', 'Pelawat', 3.5),
],
},
HT_HANDICAP: {
marketKey: 'HT_HANDICAP',
period: 'HT',
sortOrder: 6,
defaultLineValue: -0.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'HANDICAP',
renderType: 'standard',
detailOrder: 7,
parlayOrder: 5,
i18nKey: 'bet.market_ht_handicap',
adminI18nKey: 'matchEditor.market.HT_HANDICAP',
nameI18n: text('半场让球', 'HT Handicap', 'Handicap Separuh'),
selectionTemplate: [
selection('HOME', handicapName('home', -0.5, true), 'Home', 'Tuan Rumah', 1.9),
selection('AWAY', handicapName('away', -0.5, true), 'Away', 'Pelawat', 1.9),
],
},
HT_OVER_UNDER: {
marketKey: 'HT_OVER_UNDER',
period: 'HT',
sortOrder: 7,
defaultLineValue: 1.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'TOTAL',
renderType: 'standard',
detailOrder: 8,
parlayOrder: 6,
i18nKey: 'bet.market_ht_ou',
adminI18nKey: 'matchEditor.market.HT_OVER_UNDER',
nameI18n: text('半场大小', 'HT Over/Under', 'Atas/Bawah Separuh'),
selectionTemplate: [
selection('OVER', totalName('over', 1.5, true), 'Over', 'Atas', 2),
selection('UNDER', totalName('under', 1.5, true), 'Under', 'Bawah', 1.75),
],
},
FT_CORRECT_SCORE: {
marketKey: 'FT_CORRECT_SCORE',
period: 'FT',
sortOrder: 8,
defaultLineValue: null,
allowSingle: true,
allowParlay: false,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'CORRECT_SCORE',
renderType: 'correctScore',
detailOrder: 0,
parlayOrder: null,
i18nKey: 'bet.market_cs',
adminI18nKey: 'matchEditor.market.FT_CORRECT_SCORE',
nameI18n: text('全场波胆', 'FT Correct Score', 'Skor Tepat Penuh'),
selectionTemplate: ftCorrectScoreSelections,
},
HT_CORRECT_SCORE: {
marketKey: 'HT_CORRECT_SCORE',
period: 'HT',
sortOrder: 9,
defaultLineValue: null,
allowSingle: true,
allowParlay: false,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'CORRECT_SCORE',
renderType: 'correctScore',
detailOrder: 1,
parlayOrder: null,
i18nKey: 'bet.market_ht_cs',
adminI18nKey: 'matchEditor.market.HT_CORRECT_SCORE',
nameI18n: text('上半场波胆', '1H Correct Score', 'Skor Tepat Separuh Pertama'),
selectionTemplate: htCorrectScoreSelections,
},
SH_CORRECT_SCORE: {
marketKey: 'SH_CORRECT_SCORE',
period: 'SH',
sortOrder: 10,
defaultLineValue: null,
allowSingle: true,
allowParlay: false,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'CORRECT_SCORE',
renderType: 'correctScore',
detailOrder: 2,
parlayOrder: null,
i18nKey: 'bet.market_sh_cs',
adminI18nKey: 'matchEditor.market.SH_CORRECT_SCORE',
nameI18n: text('下半场波胆', '2H Correct Score', 'Skor Tepat Separuh Kedua'),
selectionTemplate: htCorrectScoreSelections,
},
OUTRIGHT_WINNER: {
marketKey: 'OUTRIGHT_WINNER',
period: 'OUTRIGHT',
sortOrder: 1,
defaultLineValue: null,
allowSingle: true,
allowParlay: false,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'OUTRIGHT',
renderType: 'standard',
detailOrder: null,
parlayOrder: null,
i18nKey: 'bet.market_outright_winner',
adminI18nKey: 'matchEditor.market.OUTRIGHT_WINNER',
nameI18n: text('冠军', 'Outright Winner', 'Juara'),
selectionTemplate: [],
},
FT_TEAM_TOTAL_HOME: {
marketKey: 'FT_TEAM_TOTAL_HOME',
period: 'FT',
sortOrder: 40,
defaultLineValue: 1.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'TOTAL',
renderType: 'standard',
detailOrder: 10,
parlayOrder: 8,
i18nKey: 'bet.market_ft_team_total_home',
adminI18nKey: 'matchEditor.market.FT_TEAM_TOTAL_HOME',
nameI18n: text('主队进球大小', 'Home Team Total Goals', 'Jumlah Gol Tuan Rumah'),
selectionTemplate: teamTotalSelections,
},
FT_TEAM_TOTAL_AWAY: {
marketKey: 'FT_TEAM_TOTAL_AWAY',
period: 'FT',
sortOrder: 41,
defaultLineValue: 1.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'TOTAL',
renderType: 'standard',
detailOrder: 11,
parlayOrder: 9,
i18nKey: 'bet.market_ft_team_total_away',
adminI18nKey: 'matchEditor.market.FT_TEAM_TOTAL_AWAY',
nameI18n: text('客队进球大小', 'Away Team Total Goals', 'Jumlah Gol Pelawat'),
selectionTemplate: teamTotalSelections,
},
HT_FT: {
marketKey: 'HT_FT',
period: 'FT',
sortOrder: 42,
defaultLineValue: null,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'SCORE',
renderType: 'standard',
detailOrder: 12,
parlayOrder: 10,
i18nKey: 'bet.market_ht_ft',
adminI18nKey: 'matchEditor.market.HT_FT',
nameI18n: text('半全场', 'Half Time / Full Time', 'Separuh Masa / Penuh Masa'),
selectionTemplate: htFtSelections,
},
FT_TOTAL_GOALS: {
marketKey: 'FT_TOTAL_GOALS',
period: 'FT',
sortOrder: 43,
defaultLineValue: null,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'SCORE',
renderType: 'standard',
detailOrder: 13,
parlayOrder: 11,
i18nKey: 'bet.market_ft_total_goals',
adminI18nKey: 'matchEditor.market.FT_TOTAL_GOALS',
nameI18n: text('总进球数', 'Total Goals', 'Jumlah Gol'),
selectionTemplate: totalGoalsSelections,
},
FT_CORNERS_HANDICAP: {
marketKey: 'FT_CORNERS_HANDICAP',
period: 'FT',
sortOrder: 60,
defaultLineValue: -0.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'MANUAL_STATS',
renderType: 'standard',
detailOrder: 14,
parlayOrder: 12,
i18nKey: 'bet.market_ft_corners_handicap',
adminI18nKey: 'matchEditor.market.FT_CORNERS_HANDICAP',
nameI18n: text('全场角球让球', 'FT Corners Handicap', 'Handicap Sepakan Sudut Penuh'),
selectionTemplate: cornersHandicapSelections,
},
FT_CORNERS_OVER_UNDER: {
marketKey: 'FT_CORNERS_OVER_UNDER',
period: 'FT',
sortOrder: 61,
defaultLineValue: 8.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'MANUAL_STATS',
renderType: 'standard',
detailOrder: 15,
parlayOrder: 13,
i18nKey: 'bet.market_ft_corners_ou',
adminI18nKey: 'matchEditor.market.FT_CORNERS_OVER_UNDER',
nameI18n: text('全场角球大小', 'FT Corners Over/Under', 'Atas/Bawah Sudut Penuh'),
selectionTemplate: cornersTotalSelections,
},
FT_CARDS_OVER_UNDER: {
marketKey: 'FT_CARDS_OVER_UNDER',
period: 'FT',
sortOrder: 70,
defaultLineValue: 3.5,
allowSingle: true,
allowParlay: true,
showOnPlayer: true,
settlementSupported: true,
settlementKind: 'MANUAL_STATS',
renderType: 'standard',
detailOrder: 16,
parlayOrder: 14,
i18nKey: 'bet.market_ft_cards_ou',
adminI18nKey: 'matchEditor.market.FT_CARDS_OVER_UNDER',
nameI18n: text('全场罚牌大小', 'FT Cards Over/Under', 'Atas/Bawah Kad Penuh'),
selectionTemplate: cardsTotalSelections,
},
} as const satisfies Record<string, FootballMarketCatalogEntry>;
export type FootballMarketType = keyof typeof FOOTBALL_MARKET_CATALOG;
export const FEATURED_MARKET_TYPES = Object.entries(FOOTBALL_MARKET_CATALOG)
.filter(([, entry]) => entry.detailOrder !== null && entry.detailOrder < 3)
.sort(([, a], [, b]) => (a.detailOrder ?? 0) - (b.detailOrder ?? 0))
.map(([marketType]) => marketType) as FootballMarketType[];
export const DETAIL_MARKET_TYPES = Object.entries(FOOTBALL_MARKET_CATALOG)
.filter(([, entry]) => entry.detailOrder !== null)
.sort(([, a], [, b]) => (a.detailOrder ?? 0) - (b.detailOrder ?? 0))
.map(([marketType]) => marketType) as FootballMarketType[];
export const GRID_MARKET_TYPES = DETAIL_MARKET_TYPES.filter(
(marketType) => !FEATURED_MARKET_TYPES.includes(marketType),
);
export const PARLAY_MARKET_TYPES = Object.entries(FOOTBALL_MARKET_CATALOG)
.filter(([, entry]) => entry.parlayOrder !== null && entry.allowParlay)
.sort(([, a], [, b]) => (a.parlayOrder ?? 0) - (b.parlayOrder ?? 0))
.map(([marketType]) => marketType) as FootballMarketType[];
export const DEFAULT_MARKET_EXCLUDED_TYPES = [
'HT_CORRECT_SCORE',
'SH_CORRECT_SCORE',
] as const satisfies readonly FootballMarketType[];
export const DEFAULT_MARKET_TYPES = Object.entries(FOOTBALL_MARKET_CATALOG)
.filter(
([marketType, entry]) =>
entry.showOnPlayer &&
entry.marketKey !== 'OUTRIGHT_WINNER' &&
!(DEFAULT_MARKET_EXCLUDED_TYPES as readonly string[]).includes(marketType),
)
.sort(([, a], [, b]) => a.sortOrder - b.sortOrder)
.map(([marketType]) => marketType) as FootballMarketType[];
export const MARKET_I18N_KEY: Record<string, string> = Object.fromEntries(
Object.entries(FOOTBALL_MARKET_CATALOG).map(([marketType, entry]) => [
marketType,
entry.i18nKey,
]),
);
export function isFootballMarketType(marketType: string): marketType is FootballMarketType {
return marketType in FOOTBALL_MARKET_CATALOG;
}
export function isCorrectScoreMarketType(marketType: string) {
return ['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE'].includes(marketType);
}
export function isSettlementSupportedMarketType(marketType: string) {
return isFootballMarketType(marketType) && FOOTBALL_MARKET_CATALOG[marketType].settlementSupported;
}
export function marketUsesLineValue(marketType: string) {
return isFootballMarketType(marketType) && FOOTBALL_MARKET_CATALOG[marketType].defaultLineValue !== null;
}
export function sanitizeLocalizedText(input: unknown): LocalizedText {
if (!input || typeof input !== 'object') return {};
const out: LocalizedText = {};
for (const locale of MARKET_LOCALES) {
const raw = (input as Record<string, unknown>)[locale];
if (raw == null) continue;
const value = String(raw).trim();
if (value) out[locale] = value;
}
return out;
}
export function resolveMarketText(input: unknown, locale: string, fallback?: LocalizedText | string | null) {
const map = sanitizeLocalizedText(input);
if (fallback && typeof fallback === 'object') {
Object.assign(map, Object.fromEntries(
Object.entries(fallback).filter(([key]) => map[key as MarketLocale] == null),
));
}
const resolved = resolveTranslationFallback(map, locale);
if (resolved) return resolved;
if (typeof fallback === 'string') return fallback;
return '';
}
export function defaultMarketName(marketType: string, locale = 'zh-CN') {
if (!isFootballMarketType(marketType)) return marketType;
return resolveMarketText(FOOTBALL_MARKET_CATALOG[marketType].nameI18n, locale, marketType);
}
export function defaultSelectionName(
marketType: string,
selectionCode: string,
locale = 'zh-CN',
) {
if (!isFootballMarketType(marketType)) return selectionCode;
const hit = FOOTBALL_MARKET_CATALOG[marketType].selectionTemplate.find(
(s) => s.code === selectionCode,
);
return hit ? resolveMarketText(hit.nameI18n, locale, hit.name) : selectionCode;
}
export function buildMarketLineKey(
marketType: string,
lineValue?: number | string | null,
params?: Record<string, unknown> | null,
) {
const line = lineValue == null || lineValue === '' ? 'none' : Number(lineValue).toFixed(2);
const paramPairs = Object.entries(params ?? {})
.filter(([, value]) => value != null && value !== '')
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${String(value)}`)
.join('|');
return paramPairs ? `${marketType}:${line}:${paramPairs}` : `${marketType}:${line}`;
}
export function buildMarketTemplate(marketType: string): FootballMarketCatalogEntry {
if (!isFootballMarketType(marketType)) {
throw new Error(`Unknown football market type: ${marketType}`);
}
return FOOTBALL_MARKET_CATALOG[marketType];
}