diff --git a/src/api/admin-bet-providers.ts b/src/api/admin-bet-providers.ts new file mode 100644 index 0000000..1004f7c --- /dev/null +++ b/src/api/admin-bet-providers.ts @@ -0,0 +1,26 @@ +import { adminRequest } from "@/lib/admin-http"; +import type { + AdminBetProviderCreatePayload, + AdminBetProviderListData, + AdminBetProviderRow, + AdminBetProviderUpdatePayload, +} from "@/types/api/admin-bet-provider"; + +const A = `/admin`; + +export async function getAdminBetProviders(): Promise { + return adminRequest.get(`${A}/bet-providers`); +} + +export async function postAdminBetProvider( + body: AdminBetProviderCreatePayload, +): Promise { + return adminRequest.post(`${A}/bet-providers`, body); +} + +export async function putAdminBetProvider( + id: number, + body: AdminBetProviderUpdatePayload, +): Promise { + return adminRequest.put(`${A}/bet-providers/${id}`, body); +} diff --git a/src/api/admin-tickets.ts b/src/api/admin-tickets.ts index 19c9756..7666b50 100644 --- a/src/api/admin-tickets.ts +++ b/src/api/admin-tickets.ts @@ -13,6 +13,7 @@ export type TicketItemsListQuery = { site_code?: string; agent_node_id?: number; draw_no?: string; + provider_code?: string; status?: string[]; number?: string; start_date?: string; diff --git a/src/app/admin/(shell)/config/bet-providers/page.tsx b/src/app/admin/(shell)/config/bet-providers/page.tsx new file mode 100644 index 0000000..61c6ef7 --- /dev/null +++ b/src/app/admin/(shell)/config/bet-providers/page.tsx @@ -0,0 +1,15 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { BetProviderSettingsPanel } from "@/modules/config/bet-provider-settings-panel"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_BET_PROVIDER_MANAGE } from "@/lib/admin-prd"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("config", "betProviders.title"); + +export default function AdminBetProvidersPage() { + return ( + + + + ); +} diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index ddbfa2a..9050896 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -58,6 +58,8 @@ "currenciesTitle": "Currencies", "jackpotDesc": "Pool parameters and ledger records", "jackpotTitle": "Jackpot", + "betProvidersDesc": "Manage selectable betting providers", + "betProvidersTitle": "Bet providers", "oddsDesc": "Odds matrix and base rebate in one version stream", "oddsTitle": "Odds & base rebate", "playsDesc": "Play switches, limits, and rule copy", @@ -68,6 +70,9 @@ "settingsTitle": "System settings", "title": "Operations configuration" }, + "betProviders": { + "title": "Bet providers" + }, "integrationSites": { "adminAccountCreated": "Created site admin account {{username}}", "adminAccountSectionDescription": "Creating a site will also create one admin account bound to that site.", diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 7df3fe0..0b48fc2 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -58,6 +58,8 @@ "currenciesTitle": "मुद्रा व्यवस्थापन", "jackpotDesc": "पूल प्यारामिटर र लेजर", "jackpotTitle": "ज्याकपट", + "betProvidersDesc": "छनोट गर्न मिल्ने बेट प्रदायक व्यवस्थापन", + "betProvidersTitle": "बेट प्रदायक", "oddsDesc": "बाधा म्याट्रिक्स र रिबेट, एउटै संस्करण", "oddsTitle": "बाधा र रिबेट", "playsDesc": "खेल स्विच, सीमा र नियम पाठ", @@ -68,6 +70,9 @@ "settingsTitle": "प्रणाली सेटिङ", "title": "सञ्चालन कन्फिगरेसन अवलोकन" }, + "betProviders": { + "title": "बेट प्रदायक" + }, "integrationSites": { "adminAccountCreated": "साइट एडमिन खाता {{username}} सिर्जना भयो", "adminAccountSectionDescription": "साइट सिर्जना गर्दा त्यस साइटसँग बाँधिएको एउटा एडमिन खाता पनि सिर्जना हुन्छ।", diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index dd59a99..bba28d5 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -29,6 +29,8 @@ "oddsDesc": "赔率矩阵与基础回水,版本一体发布", "jackpotTitle": "奖池", "jackpotDesc": "奖池参数与进账流水", + "betProvidersTitle": "开注商管理", + "betProvidersDesc": "维护玩家可勾选的下注公司", "riskCapTitle": "限额版本", "riskCapDesc": "号码赔付封顶与占用视图", "integrationTitle": "接入站点", @@ -38,6 +40,9 @@ "currenciesTitle": "币种管理", "currenciesDesc": "维护支持的币种与精度" }, + "betProviders": { + "title": "开注商管理" + }, "integrationSites": { "title": "接入站点", "description": "由运营在后台维护各主站对接参数,并通过权限控制谁能查看或修改。site_code 创建后不可修改。", diff --git a/src/lib/admin-prd.ts b/src/lib/admin-prd.ts index e165847..01a1da5 100644 --- a/src/lib/admin-prd.ts +++ b/src/lib/admin-prd.ts @@ -24,6 +24,7 @@ export const PRD_DRAW_RESULT_VIEW = "prd.draw_result.view" as const; export const PRD_DRAW_REOPEN_MANAGE = "prd.draw_reopen.manage" as const; export const PRD_PLAY_SWITCH_MANAGE = "prd.play_switch.manage" as const; +export const PRD_BET_PROVIDER_MANAGE = "prd.bet_provider.manage" as const; export const PRD_ODDS_MANAGE = "prd.odds.manage" as const; export const PRD_REBATE_MANAGE = "prd.rebate.manage" as const; export const PRD_REBATE_VIEW = "prd.rebate.view" as const; @@ -169,6 +170,7 @@ export const PRD_INTEGRATION_ACCESS_ANY = [PRD_INTEGRATION_VIEW, PRD_INTEGRATION /** 运营配置首页 */ export const PRD_CONFIG_HUB_ACCESS_ANY = [ ...PRD_RULES_PLAYS_ACCESS_ANY, + PRD_BET_PROVIDER_MANAGE, ...PRD_RULES_ODDS_ACCESS_ANY, ...PRD_JACKPOT_ACCESS_ANY, ...PRD_RISK_CAP_ACCESS_ANY, diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 61b2278..55a85fb 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -1,5 +1,6 @@ import type { LucideIcon } from "lucide-react"; import { + Building2, CalendarClock, ClipboardList, Coins, @@ -34,6 +35,7 @@ export const adminNavIconBySegment: Record players: Users, draws: CalendarClock, rules_plays: ClipboardList, + bet_providers: Building2, rules_odds: Percent, jackpot: Trophy, risk_cap: ShieldAlert, diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index e36548d..01cd102 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -14,6 +14,7 @@ export type AdminNavSegment = | "players" | "draws" | "rules_plays" + | "bet_providers" | "rules_odds" | "jackpot" | "risk_cap" diff --git a/src/modules/config/bet-provider-settings-panel.tsx b/src/modules/config/bet-provider-settings-panel.tsx new file mode 100644 index 0000000..d6d8f82 --- /dev/null +++ b/src/modules/config/bet-provider-settings-panel.tsx @@ -0,0 +1,308 @@ +"use client"; + +import { Pencil } from "lucide-react"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { + getAdminBetProviders, + postAdminBetProvider, + putAdminBetProvider, +} from "@/api/admin-bet-providers"; +import { AdminPageCard } from "@/components/admin/admin-page-card"; +import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { useTranslationRef } from "@/hooks/use-translation-ref"; +import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminBetProviderRow } from "@/types/api/admin-bet-provider"; + +type ProviderFormState = { + code: string; + name: string; + is_enabled: boolean; + sort_order: string; +}; + +const EMPTY_FORM: ProviderFormState = { + code: "", + name: "", + is_enabled: true, + sort_order: "0", +}; + +function toFormState(row: AdminBetProviderRow): ProviderFormState { + return { + code: row.code, + name: row.name, + is_enabled: row.is_enabled, + sort_order: String(row.sort_order), + }; +} + +function parseSortOrder(value: string): number { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0; +} + +export function BetProviderSettingsPanel() { + const { t } = useTranslation(["config", "adminUsers"]); + const tRef = useTranslationRef(["config", "adminUsers"]); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [mode, setMode] = useState<"create" | "edit">("create"); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState(EMPTY_FORM); + + const load = useCallback(async () => { + setLoading(true); + try { + const data = await getAdminBetProviders(); + setItems(data.items); + } catch (error) { + toast.error( + error instanceof LotteryApiBizError + ? error.message + : tRef.current("betProviders.loadFailed", { + ns: "config", + defaultValue: "加载开注商失败", + }), + ); + } finally { + setLoading(false); + } + }, [tRef]); + + useAsyncEffect(() => { + void load(); + }, []); + + function openCreate(): void { + setMode("create"); + setEditingId(null); + setForm(EMPTY_FORM); + setDialogOpen(true); + } + + function openEdit(row: AdminBetProviderRow): void { + setMode("edit"); + setEditingId(row.id); + setForm(toFormState(row)); + setDialogOpen(true); + } + + function updateForm(key: K, value: ProviderFormState[K]): void { + setForm((prev) => ({ ...prev, [key]: value })); + } + + async function handleSubmit(): Promise { + const name = form.name.trim(); + const sortOrder = parseSortOrder(form.sort_order); + if (name === "" || (mode === "create" && form.code.trim() === "")) { + toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码和名称" })); + return; + } + + setSaving(true); + try { + if (mode === "create") { + await postAdminBetProvider({ + code: form.code.trim().toUpperCase(), + name, + is_enabled: form.is_enabled, + sort_order: sortOrder, + }); + toast.success(t("betProviders.createSuccess", { ns: "config", defaultValue: "已创建开注商" })); + } else if (editingId !== null) { + await putAdminBetProvider(editingId, { + name, + is_enabled: form.is_enabled, + sort_order: sortOrder, + }); + toast.success(t("betProviders.updateSuccess", { ns: "config", defaultValue: "已更新开注商" })); + } + setDialogOpen(false); + await load(); + } catch (error) { + toast.error( + error instanceof LotteryApiBizError + ? error.message + : t("betProviders.saveFailed", { ns: "config", defaultValue: "保存开注商失败" }), + ); + } finally { + setSaving(false); + } + } + + return ( + <> + {t("betProviders.create", { ns: "config", defaultValue: "新增开注商" })}} + > +
+ + + + {t("betProviders.table.code", { ns: "config", defaultValue: "代码" })} + {t("betProviders.table.name", { ns: "config", defaultValue: "名称" })} + {t("betProviders.table.enabled", { ns: "config", defaultValue: "状态" })} + {t("betProviders.table.sort", { ns: "config", defaultValue: "排序" })} + + {t("betProviders.table.actions", { ns: "config", defaultValue: "操作" })} + + + + + {loading ? ( + + + {t("betProviders.loading", { ns: "config", defaultValue: "加载中..." })} + + + ) : items.length === 0 ? ( + + ) : ( + items.map((row) => ( + + {row.code} + + {row.name} + {row.is_default ? ( + + {t("betProviders.default", { ns: "config", defaultValue: "默认" })} + + ) : null} + + + + {row.is_enabled + ? t("system.states.enabled", { ns: "config" }) + : t("system.states.disabled", { ns: "config" })} + + + {row.sort_order} + + openEdit(row), + }, + ]} + /> + + + )) + )} + +
+
+
+ + + + + + {t(mode === "create" ? "betProviders.dialog.createTitle" : "betProviders.dialog.editTitle", { + ns: "config", + defaultValue: mode === "create" ? "新增开注商" : "编辑开注商", + })} + + + {t("betProviders.dialog.description", { + ns: "config", + defaultValue: "代码创建后不可修改;停用后玩家端不可再选择。", + })} + + + +
+
+ + updateForm("code", e.target.value.toUpperCase())} + disabled={saving || mode === "edit"} + /> +
+
+ + updateForm("name", e.target.value)} + disabled={saving} + /> +
+
+ + updateForm("sort_order", e.target.value)} + disabled={saving} + /> +
+
+
+

{t("betProviders.form.enabled", { ns: "config", defaultValue: "启用" })}

+

+ {t("betProviders.form.enabledHint", { ns: "config", defaultValue: "关闭后不会出现在玩家端可选公司里" })} +

+
+ updateForm("is_enabled", checked)} + disabled={saving} + aria-label={t("betProviders.form.enabled", { ns: "config", defaultValue: "启用" })} + /> +
+
+ + +
+
+
+
+ + ); +} diff --git a/src/modules/config/config-hub-screen.tsx b/src/modules/config/config-hub-screen.tsx index 1407aef..cd1d03a 100644 --- a/src/modules/config/config-hub-screen.tsx +++ b/src/modules/config/config-hub-screen.tsx @@ -6,7 +6,12 @@ import { ChevronRight } from "lucide-react"; import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { PRD_INTEGRATION_ACCESS_ANY, PRD_SETTINGS_ACCESS_ANY, PRD_CURRENCY_MANAGE } from "@/lib/admin-prd"; +import { + PRD_BET_PROVIDER_MANAGE, + PRD_CURRENCY_MANAGE, + PRD_INTEGRATION_ACCESS_ANY, + PRD_SETTINGS_ACCESS_ANY, +} from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; @@ -36,6 +41,12 @@ const HUB_CARDS: HubCard[] = [ descKey: "hub.jackpotDesc", requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"], }, + { + href: "/admin/config/bet-providers", + titleKey: "hub.betProvidersTitle", + descKey: "hub.betProvidersDesc", + requiredAny: [PRD_BET_PROVIDER_MANAGE], + }, { href: "/admin/risk/cap", titleKey: "hub.riskCapTitle", diff --git a/src/modules/players/player-detail-console.tsx b/src/modules/players/player-detail-console.tsx index 1497d67..afe140e 100644 --- a/src/modules/players/player-detail-console.tsx +++ b/src/modules/players/player-detail-console.tsx @@ -58,6 +58,10 @@ function ticketStatusText(status: string, t: (key: string, opts?: { ns?: string return translated === key ? status : translated; } +function providerLabel(providerName?: string | null, providerCode?: string | null): string { + return providerName || providerCode || "—"; +} + function walletStatusLabel(status: string, t: (key: string, opts?: { ns?: string }) => string): string { switch (status) { case "processing": @@ -438,6 +442,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) { {t("ticketNo", { ns: "tickets" })} {t("orderNo", { ns: "tickets" })} {t("drawNo", { ns: "tickets" })} + {t("provider", { ns: "tickets", defaultValue: "开注商" })} {t("playCode", { ns: "tickets" })} {t("number", { ns: "tickets" })} {t("actualDeduct", { ns: "tickets" })} @@ -450,7 +455,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) { {ticketsLoading && tickets.length === 0 ? ( - + ) : null} {tickets.map((row) => ( @@ -464,6 +469,9 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) { {row.order_no ?? "—"} {row.draw_no ?? "—"} + + {providerLabel(row.provider_name, row.provider_code)} + {playCodeLabel(row.play_code)} {row.original_number ?? "—"} @@ -491,7 +499,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) { ))} {!ticketsLoading && tickets.length === 0 ? ( - + ) : null} diff --git a/src/modules/tickets/player-tickets-console.tsx b/src/modules/tickets/player-tickets-console.tsx index 9c07bbc..09dbe13 100644 --- a/src/modules/tickets/player-tickets-console.tsx +++ b/src/modules/tickets/player-tickets-console.tsx @@ -61,6 +61,7 @@ const TICKET_STATUS_OPTIONS = [ type TicketFilters = { playerQuery: string; drawNo: string; + providerCode: string; numberKeyword: string; startDate: string; endDate: string; @@ -70,6 +71,7 @@ type TicketFilters = { const emptyTicketFilters: TicketFilters = { playerQuery: "", drawNo: "", + providerCode: "", numberKeyword: "", startDate: "", endDate: "", @@ -103,6 +105,10 @@ function ticketDetailPath(ticketNo: string): string { return `/admin/tickets/${encodeURIComponent(ticketNo)}`; } +function providerLabel(providerName?: string | null, providerCode?: string | null): string { + return providerName || providerCode || "—"; +} + function TicketFilterField({ id, label, @@ -164,6 +170,7 @@ export function PlayerTicketsConsole(): React.ReactElement { per_page: perPage, ...query, draw_no: applied.drawNo.trim() || undefined, + provider_code: applied.providerCode.trim().toUpperCase() || undefined, status: applied.statuses.length > 0 ? applied.statuses : undefined, number: applied.numberKeyword.trim() || undefined, start_date: applied.startDate || undefined, @@ -188,6 +195,7 @@ export function PlayerTicketsConsole(): React.ReactElement { ...draft, playerQuery: draft.playerQuery.trim(), drawNo: draft.drawNo.trim(), + providerCode: draft.providerCode.trim().toUpperCase(), numberKeyword: draft.numberKeyword.trim(), }); setPage(1); @@ -263,6 +271,22 @@ export function PlayerTicketsConsole(): React.ReactElement { }} /> + + + setDraft((current) => ({ ...current, providerCode: e.target.value.toUpperCase() })) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + runSearch(); + } + }} + /> + - {applied.playerQuery || applied.drawNo || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? ( + {applied.playerQuery || applied.drawNo || applied.providerCode || applied.numberKeyword || applied.startDate || applied.endDate || applied.statuses.length > 0 ? (

{applied.playerQuery ? ( <> @@ -337,6 +361,12 @@ export function PlayerTicketsConsole(): React.ReactElement { {t("drawNo")}:{applied.drawNo} ) : null} + {applied.providerCode ? ( + <> + {" · "} + {t("provider", { defaultValue: "开注商" })}:{applied.providerCode} + + ) : null}

) : null} @@ -353,6 +383,7 @@ export function PlayerTicketsConsole(): React.ReactElement { {t("fundingMode", { ns: "players" })} {t("orderNo")} {t("drawNo")} + {t("provider", { defaultValue: "开注商" })} {t("playCode")} {t("number")} {t("betAmount")} @@ -367,9 +398,9 @@ export function PlayerTicketsConsole(): React.ReactElement { {loading && !data ? ( - + ) : !data || data.items.length === 0 ? ( - + ) : ( data.items.map((row) => { const winLabel = row.jackpot_win_amount > 0 @@ -392,6 +423,9 @@ export function PlayerTicketsConsole(): React.ReactElement { {row.order_no ?? "—"} {row.draw_no ?? "—"} + + {providerLabel(row.provider_name, row.provider_code)} + {playCodeLabel(row.play_code)} {row.original_number ?? "—"} diff --git a/src/modules/tickets/ticket-detail-console.tsx b/src/modules/tickets/ticket-detail-console.tsx index 7dfffab..1248f77 100644 --- a/src/modules/tickets/ticket-detail-console.tsx +++ b/src/modules/tickets/ticket-detail-console.tsx @@ -37,6 +37,10 @@ function ticketStatusText(value: string, t: (key: string) => string): string { return translated === key ? value : translated; } +function providerLabel(providerName?: string | null, providerCode?: string | null): string { + return providerName || providerCode || "—"; +} + export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) { const { t } = useTranslation(["tickets", "common"]); const tRef = useTranslationRef(["tickets", "common"]); @@ -126,6 +130,12 @@ export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) { {t("number")}: {data.original_number ?? "—"}

+

+ {t("provider", { defaultValue: "开注商" })}: + + {providerLabel(data.provider_name, data.provider_code)} + +

{t("combinationCount")}: {data.combination_count} diff --git a/src/types/api/admin-bet-provider.ts b/src/types/api/admin-bet-provider.ts new file mode 100644 index 0000000..a666cd3 --- /dev/null +++ b/src/types/api/admin-bet-provider.ts @@ -0,0 +1,27 @@ +export type AdminBetProviderRow = { + id: number; + code: string; + name: string; + is_enabled: boolean; + sort_order: number; + is_default: boolean; + created_at: string | null; + updated_at: string | null; +}; + +export type AdminBetProviderListData = { + items: AdminBetProviderRow[]; +}; + +export type AdminBetProviderCreatePayload = { + code: string; + name: string; + is_enabled?: boolean; + sort_order?: number; +}; + +export type AdminBetProviderUpdatePayload = { + name?: string; + is_enabled?: boolean; + sort_order?: number; +}; diff --git a/src/types/api/admin-player-tickets.ts b/src/types/api/admin-player-tickets.ts index 52df231..d85df19 100644 --- a/src/types/api/admin-player-tickets.ts +++ b/src/types/api/admin-player-tickets.ts @@ -3,6 +3,8 @@ export type AdminPlayerTicketItemRow = { order_no: string | null; draw_no: string | null; currency_code: string | null; + provider_code?: string | null; + provider_name?: string | null; play_code: string; original_number: string | null; /** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */ diff --git a/src/types/api/admin-tickets.ts b/src/types/api/admin-tickets.ts index 2afe8fb..2bbc54a 100644 --- a/src/types/api/admin-tickets.ts +++ b/src/types/api/admin-tickets.ts @@ -14,6 +14,8 @@ export type AdminTicketItemRow = { order_no: string | null; draw_no: string | null; currency_code: string | null; + provider_code?: string | null; + provider_name?: string | null; play_code: string; original_number: string | null; /** @deprecated 历史字段,单位为 minor;优先使用 total_bet_amount_minor */ @@ -63,6 +65,8 @@ export type AdminTicketItemDetail = { agent_node_id: number | null; agent_code: string | null; agent_name: string | null; + provider_code?: string | null; + provider_name?: string | null; play_code: string; dimension: number | null; digit_slot: number | null;