重构
This commit is contained in:
634
packages/shared/src/market-catalog.ts
Normal file
634
packages/shared/src/market-catalog.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user