635 lines
21 KiB
TypeScript
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];
|
|
}
|