import { PrismaClient } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { syncWc2026OutrightMarket } from '../src/domains/catalog/wc2026-outright.sync'; const prisma = new PrismaClient(); /** 为演示赛事补齐详情页玩法(与后台 markets 模板一致) */ async function seedDemoMarkets(matchId: bigint) { const configs: Array<{ marketType: string; period: string; lineValue?: number; sortOrder: number; selections: Array<{ code: string; name: string; odds: number }>; }> = [ { marketType: 'FT_1X2', period: 'FT', sortOrder: 1, selections: [ { code: 'HOME', name: '主胜', odds: 2.5 }, { code: 'DRAW', name: '和', odds: 3.2 }, { code: 'AWAY', name: '客胜', odds: 2.8 }, ], }, { marketType: 'FT_HANDICAP', period: 'FT', lineValue: -0.5, sortOrder: 2, selections: [ { code: 'HOME', name: '主 -0.5', odds: 1.9 }, { code: 'AWAY', name: '客 +0.5', odds: 1.9 }, ], }, { marketType: 'FT_OVER_UNDER', period: 'FT', lineValue: 2.5, sortOrder: 3, selections: [ { code: 'OVER', name: '大 2.5', odds: 1.85 }, { code: 'UNDER', name: '小 2.5', odds: 1.95 }, ], }, { marketType: 'FT_ODD_EVEN', period: 'FT', sortOrder: 4, selections: [ { code: 'ODD', name: '单', odds: 1.9 }, { code: 'EVEN', name: '双', odds: 1.9 }, ], }, { marketType: 'HT_1X2', period: 'HT', sortOrder: 5, selections: [ { code: 'HOME', name: '半场主', odds: 3.0 }, { code: 'DRAW', name: '半场和', odds: 2.0 }, { code: 'AWAY', name: '半场客', odds: 3.5 }, ], }, { marketType: 'HT_HANDICAP', period: 'HT', lineValue: -0.5, sortOrder: 6, selections: [ { code: 'HOME', name: '半场主 -0.5', odds: 1.9 }, { code: 'AWAY', name: '半场客 +0.5', odds: 1.9 }, ], }, { marketType: 'HT_OVER_UNDER', period: 'HT', lineValue: 1.5, sortOrder: 7, selections: [ { code: 'OVER', name: '半场大 1.5', odds: 2.0 }, { code: 'UNDER', name: '半场小 1.5', odds: 1.75 }, ], }, { marketType: 'FT_CORRECT_SCORE', period: 'FT', sortOrder: 8, selections: [ { code: 'SCORE_1_0', name: '1-0', odds: 4.86 }, { code: 'SCORE_2_0', name: '2-0', odds: 5.22 }, { code: 'SCORE_2_1', name: '2-1', odds: 7.92 }, { code: 'SCORE_3_0', name: '3-0', odds: 8.28 }, { code: 'SCORE_0_0', name: '0-0', odds: 8.64 }, { code: 'SCORE_1_1', name: '1-1', odds: 7.47 }, { code: 'SCORE_2_2', name: '2-2', odds: 24.3 }, { code: 'SCORE_3_3', name: '3-3', odds: 175.5 }, { code: 'SCORE_0_1', name: '0-1', odds: 14.4 }, { code: 'SCORE_0_2', name: '0-2', odds: 45.9 }, { code: 'SCORE_1_2', name: '1-2', odds: 23.4 }, { code: 'SCORE_0_3', name: '0-3', odds: 207 }, ], }, { marketType: 'HT_CORRECT_SCORE', period: 'HT', sortOrder: 9, selections: [ { code: 'SCORE_1_0', name: '1-0', odds: 4.5 }, { code: 'SCORE_2_0', name: '2-0', odds: 8.0 }, { code: 'SCORE_0_0', name: '0-0', odds: 5.5 }, { code: 'SCORE_1_1', name: '1-1', odds: 7.0 }, { code: 'SCORE_0_1', name: '0-1', odds: 6.5 }, { code: 'SCORE_0_2', name: '0-2', odds: 18.0 }, ], }, { marketType: 'SH_CORRECT_SCORE', period: 'SH', sortOrder: 10, selections: [ { code: 'SCORE_1_0', name: '1-0', odds: 4.5 }, { code: 'SCORE_2_0', name: '2-0', odds: 8.0 }, { code: 'SCORE_0_0', name: '0-0', odds: 5.5 }, { code: 'SCORE_1_1', name: '1-1', odds: 7.0 }, { code: 'SCORE_0_1', name: '0-1', odds: 6.5 }, { code: 'SCORE_0_2', name: '0-2', odds: 18.0 }, ], }, ]; for (const cfg of configs) { const exists = await prisma.market.findFirst({ where: { matchId, marketType: cfg.marketType }, include: { _count: { select: { selections: true } } }, }); if (exists) { const needRefresh = cfg.marketType.includes('CORRECT_SCORE') && exists._count.selections < cfg.selections.length; if (needRefresh) { await prisma.market.delete({ where: { id: exists.id } }); } else { continue; } } await prisma.market.create({ data: { matchId, marketType: cfg.marketType, period: cfg.period, lineValue: cfg.lineValue, allowSingle: true, allowParlay: true, sortOrder: cfg.sortOrder, status: 'OPEN', selections: { create: cfg.selections.map((s, i) => ({ selectionCode: s.code, selectionName: s.name, odds: s.odds, sortOrder: i, status: 'OPEN', })), }, }, }); } } async function upsertLeagueName(leagueId: bigint, names: Record) { for (const [locale, value] of Object.entries(names)) { await prisma.entityTranslation.upsert({ where: { entityType_entityId_locale_fieldName: { entityType: 'LEAGUE', entityId: leagueId, locale, fieldName: 'name', }, }, create: { entityType: 'LEAGUE', entityId: leagueId, locale, fieldName: 'name', value }, update: { value }, }); } } async function upsertTeam( code: string, names: Record, ) { const team = await prisma.team.upsert({ where: { code }, create: { code }, update: {}, }); for (const [locale, value] of Object.entries(names)) { await prisma.entityTranslation.upsert({ where: { entityType_entityId_locale_fieldName: { entityType: 'TEAM', entityId: team.id, locale, fieldName: 'name', }, }, create: { entityType: 'TEAM', entityId: team.id, locale, fieldName: 'name', value }, update: { value }, }); } return team; } async function ensurePublishedMatch(opts: { leagueId: bigint; homeTeamId: bigint; awayTeamId: bigint; startTime: Date; isHot?: boolean; displayOrder?: number; }) { let match = await prisma.match.findFirst({ where: { leagueId: opts.leagueId, homeTeamId: opts.homeTeamId, awayTeamId: opts.awayTeamId, status: 'PUBLISHED', }, }); if (!match) { match = await prisma.match.create({ data: { leagueId: opts.leagueId, homeTeamId: opts.homeTeamId, awayTeamId: opts.awayTeamId, startTime: opts.startTime, status: 'PUBLISHED', isHot: opts.isHot ?? false, displayOrder: opts.displayOrder ?? 0, publishTime: new Date(), }, }); } else { match = await prisma.match.update({ where: { id: match.id }, data: { startTime: opts.startTime, isHot: opts.isHot ?? match.isHot, displayOrder: opts.displayOrder ?? match.displayOrder, }, }); } await seedDemoMarkets(match.id); return match; } function hoursFromNow(hours: number) { return new Date(Date.now() + hours * 3600 * 1000); } async function seedSportsDemo() { const epl = await prisma.league.upsert({ where: { code: 'EPL' }, create: { code: 'EPL' }, update: {}, }); await upsertLeagueName(epl.id, { 'zh-CN': '英超', 'en-US': 'Premier League' }); const wc = await prisma.league.upsert({ where: { code: 'WC2026' }, create: { code: 'WC2026' }, update: {}, }); await upsertLeagueName(wc.id, { 'zh-CN': '2026世界杯(在加拿大,墨西哥和美国)', 'en-US': '2026 FIFA World Cup', }); const teams: Array<[string, Record]> = [ ['MUN', { 'zh-CN': '曼联', 'en-US': 'Man United' }], ['CHE', { 'zh-CN': '切尔西', 'en-US': 'Chelsea' }], ['MEX', { 'zh-CN': '墨西哥', 'en-US': 'Mexico' }], ['RSA', { 'zh-CN': '南非', 'en-US': 'South Africa' }], ['CZE', { 'zh-CN': '捷克', 'en-US': 'Czech Republic' }], ['KOR', { 'zh-CN': '韩国', 'en-US': 'South Korea' }], ['CAN', { 'zh-CN': '加拿大', 'en-US': 'Canada' }], ['BIH', { 'zh-CN': '波黑', 'en-US': 'Bosnia' }], ['USA', { 'zh-CN': '美国', 'en-US': 'USA' }], ['PAR', { 'zh-CN': '巴拉圭', 'en-US': 'Paraguay' }], ['SUI', { 'zh-CN': '瑞士', 'en-US': 'Switzerland' }], ['BRA', { 'zh-CN': '巴西', 'en-US': 'Brazil' }], ['SCO', { 'zh-CN': '苏格兰', 'en-US': 'Scotland' }], ['TUR', { 'zh-CN': '土耳其', 'en-US': 'Turkey' }], ['ARG', { 'zh-CN': '阿根廷', 'en-US': 'Argentina' }], ['FRA', { 'zh-CN': '法国', 'en-US': 'France' }], ]; const teamMap = new Map(); for (const [code, names] of teams) { teamMap.set(code, await upsertTeam(code, names)); } const get = (code: string) => { const t = teamMap.get(code); if (!t) throw new Error(`Team ${code} missing`); return t; }; // 英超:明日开赛 → 早盘 await ensurePublishedMatch({ leagueId: epl.id, homeTeamId: get('MUN').id, awayTeamId: get('CHE').id, startTime: hoursFromNow(26), isHot: true, displayOrder: 1, }); // 英超:今晚开赛 → 今日 await ensurePublishedMatch({ leagueId: epl.id, homeTeamId: get('CHE').id, awayTeamId: get('MUN').id, startTime: hoursFromNow(8), isHot: false, displayOrder: 2, }); const wcFixtures: Array<{ home: string; away: string; start: Date; hot?: boolean; order: number; }> = [ { home: 'MEX', away: 'RSA', start: new Date('2026-06-12T03:00:00Z'), hot: true, order: 1 }, { home: 'CZE', away: 'KOR', start: new Date('2026-06-12T07:00:00Z'), order: 2 }, { home: 'CAN', away: 'BIH', start: new Date('2026-06-13T00:00:00Z'), order: 3 }, { home: 'USA', away: 'PAR', start: new Date('2026-06-13T03:00:00Z'), hot: true, order: 4 }, { home: 'SUI', away: 'BRA', start: new Date('2026-06-14T16:00:00Z'), order: 5 }, { home: 'SCO', away: 'TUR', start: new Date('2026-06-14T19:00:00Z'), order: 6 }, { home: 'FRA', away: 'ARG', start: new Date('2026-06-15T20:00:00Z'), hot: true, order: 7 }, ]; for (const f of wcFixtures) { await ensurePublishedMatch({ leagueId: wc.id, homeTeamId: get(f.home).id, awayTeamId: get(f.away).id, startTime: f.start, isHot: f.hot, displayOrder: f.order, }); } console.log(` Sports demo: ${wcFixtures.length + 2} published matches`); } async function seedOutrightDemo() { const wc = await prisma.league.findUnique({ where: { code: 'WC2026' } }); if (!wc) return; const { matchId, marketId } = await syncWc2026OutrightMarket(prisma, { forceCanonical: true, }); const count = await prisma.marketSelection.count({ where: { marketId } }); console.log(` WC2026 outright: match ${matchId}, ${count} selections`); } async function seedPlayerDemo() { const player = await prisma.user.findUnique({ where: { username: 'player1' }, include: { wallet: true }, }); if (!player?.wallet) return; await prisma.wallet.update({ where: { id: player.wallet.id }, data: { availableBalance: 88888.88 }, }); await prisma.walletTransaction.upsert({ where: { transactionId: 'DEMO-DEP-001' }, create: { transactionId: 'DEMO-DEP-001', userId: player.id, walletId: player.wallet.id, transactionType: 'DEPOSIT', amount: 50000, balanceBefore: 1000, balanceAfter: 51000, frozenBefore: 0, frozenAfter: 0, remark: '演示充值', }, update: {}, }); await prisma.walletTransaction.upsert({ where: { transactionId: 'DEMO-DEP-002' }, create: { transactionId: 'DEMO-DEP-002', userId: player.id, walletId: player.wallet.id, transactionType: 'DEPOSIT', amount: 37888.88, balanceBefore: 51000, balanceAfter: 88888.88, frozenBefore: 0, frozenAfter: 0, remark: '演示充值(二笔)', }, update: {}, }); const sampleSel = await prisma.marketSelection.findFirst({ where: { status: 'OPEN', market: { marketType: 'FT_1X2', match: { status: 'PUBLISHED' } }, }, include: { market: { include: { match: true } } }, }); if (sampleSel && !(await prisma.bet.findUnique({ where: { betNo: 'DEMO-BET-001' } }))) { const odds = Number(sampleSel.odds); const stake = 200; await prisma.bet.create({ data: { betNo: 'DEMO-BET-001', userId: player.id, agentId: player.parentId, betType: 'SINGLE', stake, totalOdds: odds, potentialReturn: stake * odds, status: 'PENDING', requestId: 'seed-demo-bet-001', selections: { create: { matchId: sampleSel.market.matchId, marketId: sampleSel.marketId, selectionId: sampleSel.id, marketType: sampleSel.market.marketType, period: sampleSel.market.period, selectionNameSnapshot: sampleSel.selectionName, odds: sampleSel.odds, oddsVersion: sampleSel.oddsVersion, }, }, }, }); } const settledSel = await prisma.marketSelection.findFirst({ where: { market: { marketType: 'FT_1X2' }, selectionCode: 'DRAW', }, include: { market: true }, }); if (settledSel && !(await prisma.bet.findUnique({ where: { betNo: 'DEMO-BET-002' } }))) { const odds = Number(settledSel.odds); const stake = 50; await prisma.bet.create({ data: { betNo: 'DEMO-BET-002', userId: player.id, agentId: player.parentId, betType: 'SINGLE', stake, totalOdds: odds, potentialReturn: stake * odds, actualReturn: stake * odds, status: 'WON', settlementStatus: 'SETTLED', settledAt: new Date(Date.now() - 86400000), requestId: 'seed-demo-bet-002', selections: { create: { matchId: settledSel.market.matchId, marketId: settledSel.marketId, selectionId: settledSel.id, marketType: settledSel.market.marketType, period: settledSel.market.period, selectionNameSnapshot: settledSel.selectionName, odds: settledSel.odds, oddsVersion: settledSel.oddsVersion, resultStatus: 'WIN', effectiveOdds: settledSel.odds, }, }, }, }); } console.log(' Player demo: wallet + transactions + sample bets'); } async function main() { console.log('Seeding database...'); const superAdminRole = await prisma.role.upsert({ where: { code: 'SUPER_ADMIN' }, create: { code: 'SUPER_ADMIN', name: 'Super Admin', description: 'Full access' }, update: {}, }); const permCodes = [ 'users.create', 'users.view', 'agents.create', 'agents.view', 'wallet.deposit', 'wallet.withdraw', 'matches.manage', 'settlement.confirm', 'cashback.confirm', 'content.manage', 'reports.view', ]; for (const code of permCodes) { const perm = await prisma.permission.upsert({ where: { code }, create: { code, name: code, module: code.split('.')[0] }, update: {}, }); await prisma.rolePermission.upsert({ where: { roleId_permissionId: { roleId: superAdminRole.id, permissionId: perm.id } }, create: { roleId: superAdminRole.id, permissionId: perm.id }, update: {}, }); } const hash = await bcrypt.hash('Admin@123', 10); const agentHash = await bcrypt.hash('Agent@123', 10); const playerHash = await bcrypt.hash('Player@123', 10); await prisma.user.upsert({ where: { username: 'admin' }, create: { username: 'admin', userType: 'ADMIN', auth: { create: { passwordHash: hash } }, adminRole: { create: { roleId: superAdminRole.id } }, }, update: {}, }); const agent1 = await prisma.user.upsert({ where: { username: 'agent1' }, create: { username: 'agent1', userType: 'AGENT', agentLevel: 1, auth: { create: { passwordHash: agentHash } }, agentProfile: { create: { level: 1, creditLimit: 100000 } }, }, update: {}, }); await prisma.agentClosure.upsert({ where: { ancestorId_descendantId: { ancestorId: agent1.id, descendantId: agent1.id } }, create: { ancestorId: agent1.id, descendantId: agent1.id, depth: 0 }, update: {}, }); await prisma.user.upsert({ where: { username: 'agent2' }, create: { username: 'agent2', userType: 'AGENT', agentLevel: 2, parentId: agent1.id, auth: { create: { passwordHash: agentHash } }, agentProfile: { create: { level: 2, parentAgentId: agent1.id, creditLimit: 30000 } }, }, update: {}, }); await prisma.user.upsert({ where: { username: 'player1' }, create: { username: 'player1', userType: 'PLAYER', parentId: agent1.id, auth: { create: { passwordHash: playerHash } }, wallet: { create: { availableBalance: 1000 } }, preferences: { create: { locale: 'zh-CN', managedPassword: 'Player@123' } }, }, update: {}, }); const messages = [ { key: 'nav.home', zh: '首页', ms: 'Laman Utama', en: 'Home' }, { key: 'nav.football', zh: '足球', ms: 'Bola Sepak', en: 'Football' }, { key: 'bet.place_bet', zh: '确认下注', ms: 'Letak Pertaruhan', en: 'Place Bet' }, { key: 'error.insufficient_balance', zh: '余额不足', ms: 'Baki tidak mencukupi', en: 'Insufficient balance' }, ]; for (const m of messages) { for (const [locale, value] of [['zh-CN', m.zh], ['ms-MY', m.ms], ['en-US', m.en]] as const) { await prisma.i18nMessage.upsert({ where: { msgKey_locale: { msgKey: m.key, locale } }, create: { msgKey: m.key, locale, value }, update: { value }, }); } } await seedSportsDemo(); await seedOutrightDemo(); await seedPlayerDemo(); await prisma.content.create({ data: { contentType: 'BANNER', status: 'ACTIVE', sortOrder: 1, linkType: 'ROUTE', linkTarget: '/football', translations: { create: [ { locale: 'zh-CN', title: '欢迎投注', body: '足球赛事火热进行中', imageUrl: '/uploads/banners/welcome.svg' }, { locale: 'en-US', title: 'Welcome', body: 'Football matches available', imageUrl: '/uploads/banners/welcome.svg' }, ], }, }, }).catch(() => {}); await prisma.content.create({ data: { contentType: 'BANNER', status: 'ACTIVE', sortOrder: 2, translations: { create: [ { locale: 'zh-CN', title: '首存礼遇', body: '新会员专属优惠', imageUrl: '/uploads/banners/promo.svg' }, { locale: 'en-US', title: 'First Deposit', body: 'New member offer', imageUrl: '/uploads/banners/promo.svg' }, ], }, }, }).catch(() => {}); await prisma.content.create({ data: { contentType: 'BANNER', status: 'ACTIVE', sortOrder: 3, linkType: 'ROUTE', linkTarget: '/football', translations: { create: [ { locale: 'zh-CN', title: '热门赛事', body: '五大联赛天天有球', imageUrl: '/uploads/banners/hot-matches.svg' }, { locale: 'en-US', title: 'Hot Matches', body: 'Top leagues daily', imageUrl: '/uploads/banners/hot-matches.svg' }, ], }, }, }).catch(() => {}); await prisma.content.create({ data: { contentType: 'TICKER', status: 'ACTIVE', sortOrder: 1, translations: { create: [ { locale: 'zh-CN', body: '欢迎来到 TheBet365 · 热门赛事每日更新 · 请理性投注' }, { locale: 'en-US', body: 'Welcome to TheBet365 · Daily hot matches · Bet responsibly' }, ], }, }, }).catch(() => {}); await prisma.content.create({ data: { contentType: 'NOTICE', status: 'ACTIVE', sortOrder: 1, translations: { create: [ { locale: 'zh-CN', title: '系统维护通知:每周一 04:00-05:00 例行维护,敬请谅解' }, { locale: 'en-US', title: 'Maintenance: Every Mon 04:00-05:00 UTC' }, ], }, }, }).catch(() => {}); console.log('Seed completed! admin/Admin@123 agent1/Agent@123 player1/Player@123'); } main().catch(console.error).finally(() => prisma.$disconnect());