import { Controller, Get, Post, Put, Body, Param, Query, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard, AgentGuard } from '../../domains/identity/guards'; import { CurrentUser } from '../../shared/common/decorators'; import { jsonResponse } from '../../shared/common/filters'; import { AgentsService } from '../../domains/agent/agents.service'; import { WalletService } from '../../domains/ledger/wallet.service'; import { BetsService } from '../../domains/betting/bets.service'; import { PrismaService } from '../../shared/prisma/prisma.service'; import { IsString, IsNumber, MinLength, IsOptional, Min, IsBoolean } from 'class-validator'; class CreatePlayerDto { @IsString() username!: string; @IsString() @MinLength(8) password!: string; @IsOptional() @IsString() phone?: string; @IsOptional() @IsString() email?: string; @IsOptional() @IsString() locale?: string; @IsOptional() @IsNumber() @Min(0) initialDeposit?: number; @IsOptional() @IsString() remark?: string; } class CreateSubAgentDto extends CreatePlayerDto { @IsOptional() @IsNumber() creditLimit?: number; @IsOptional() @IsNumber() @Min(0) cashbackRate?: number; @IsOptional() @IsNumber() @Min(0) maxSingleDeposit?: number; @IsOptional() @IsNumber() @Min(0) maxDailyDeposit?: number; } class UpdatePlayerDto { @IsOptional() @IsString() username?: string; @IsOptional() @IsString() @MinLength(8) password?: string; @IsOptional() @IsString() phone?: string; @IsOptional() @IsString() email?: string; @IsOptional() @IsString() status?: string; } class TransferDto { @IsNumber() amount!: number; @IsString() requestId!: string; @IsOptional() @IsString() remark?: string; } class CreditDto extends TransferDto {} class UpdateSubAgentDto { @IsOptional() @IsString() username?: string; @IsOptional() @IsString() @MinLength(8) password?: string; @IsOptional() @IsString() phone?: string; @IsOptional() @IsString() email?: string; @IsOptional() @IsString() status?: string; @IsOptional() @IsBoolean() freezeDirectPlayers?: boolean; } @ApiTags('Agent Portal') @Controller('agent') @UseGuards(JwtAuthGuard, AgentGuard) @ApiBearerAuth() export class AgentPortalController { constructor( private agents: AgentsService, private wallet: WalletService, private bets: BetsService, private prisma: PrismaService, ) {} @Get('profile') async profile(@CurrentUser('id') agentId: bigint) { const profile = await this.agents.getProfile(agentId); return jsonResponse(profile); } @Get('players') async listPlayers(@CurrentUser('id') agentId: bigint) { const players = await this.agents.getDirectPlayers(agentId); return jsonResponse(players); } @Post('players') async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) { const user = await this.agents.createPlayer(agentId, { username: dto.username, password: dto.password, parentId: agentId, locale: dto.locale, phone: dto.phone, email: dto.email, }); if (dto.initialDeposit != null && dto.initialDeposit > 0) { await this.agents.depositToPlayer( agentId, user.id, dto.initialDeposit, `agent-create-${user.id}-${Date.now()}`, dto.remark ?? '开户初始余额', ); } const wallet = await this.prisma.wallet.findUnique({ where: { userId: user.id } }); return jsonResponse({ id: user.id, username: user.username, status: user.status, createdAt: user.createdAt, availableBalance: wallet?.availableBalance ?? 0, }); } @Get('players/:id') async getPlayer(@CurrentUser('id') agentId: bigint, @Param('id') playerId: string) { const detail = await this.agents.getDirectPlayerDetail(agentId, BigInt(playerId)); return jsonResponse(detail); } @Get('players/:id/transfer-context') async getPlayerTransferContext( @CurrentUser('id') agentId: bigint, @Param('id') playerId: string, ) { const ctx = await this.agents.getPlayerTransferContext(BigInt(playerId), { actingAgentId: agentId, }); return jsonResponse(ctx); } @Put('players/:id') async updatePlayer( @CurrentUser('id') agentId: bigint, @Param('id') playerId: string, @Body() dto: UpdatePlayerDto, ) { const detail = await this.agents.updateDirectPlayer(agentId, BigInt(playerId), dto); return jsonResponse(detail); } @Get('agents') async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) { if (level !== 1) { return jsonResponse([]); } const agents = await this.agents.listChildAgentsSummary(agentId); return jsonResponse(agents); } @Post('agents') async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) { if (level !== 1) { return jsonResponse(null, 'Only level 1 agents can create sub-agents'); } const user = await this.agents.createAgent(agentId, { username: dto.username, password: dto.password, level: 2, parentAgentId: agentId, creditLimit: dto.creditLimit, cashbackRate: dto.cashbackRate, maxSingleDeposit: dto.maxSingleDeposit, maxDailyDeposit: dto.maxDailyDeposit, }); return jsonResponse(user); } @Post('players/:id/deposit') async depositToPlayer( @CurrentUser('id') agentId: bigint, @Param('id') playerId: string, @Body() dto: TransferDto, ) { const result = await this.agents.depositToPlayer( agentId, BigInt(playerId), dto.amount, dto.requestId, dto.remark, ); return jsonResponse(result); } @Post('players/:id/withdraw') async withdrawFromPlayer( @CurrentUser('id') agentId: bigint, @Param('id') playerId: string, @Body() dto: TransferDto, ) { const result = await this.agents.withdrawFromPlayer( agentId, BigInt(playerId), dto.amount, dto.requestId, dto.remark, ); return jsonResponse(result); } @Get('agents/:id') async getSubAgent( @CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Param('id') subAgentId: string, ) { if (level !== 1) { return jsonResponse(null, 'Only level 1 agents can manage sub-agents'); } const detail = await this.agents.getSubAgentForParent(agentId, BigInt(subAgentId)); return jsonResponse(detail); } @Put('agents/:id') async updateSubAgent( @CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Param('id') subAgentId: string, @Body() dto: UpdateSubAgentDto, ) { if (level !== 1) { return jsonResponse(null, 'Only level 1 agents can manage sub-agents'); } const detail = await this.agents.updateSubAgentForParent(agentId, BigInt(subAgentId), dto); return jsonResponse(detail); } @Get('agents/:id/players') async listSubAgentPlayers( @CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Param('id') subAgentId: string, ) { if (level !== 1) { return jsonResponse([]); } await this.agents.assertDirectChildAgent(agentId, BigInt(subAgentId)); const players = await this.agents.getDirectPlayers(BigInt(subAgentId)); return jsonResponse(players); } @Post('agents/:id/credit') async allocateCredit( @CurrentUser('id') agentId: bigint, @Param('id') subAgentId: string, @Body() dto: CreditDto, ) { const subAgent = await this.prisma.agentProfile.findUnique({ where: { userId: BigInt(subAgentId) }, }); if (!subAgent || subAgent.parentAgentId !== agentId) { return jsonResponse(null, 'Not your sub-agent'); } const result = await this.agents.adjustCredit( BigInt(subAgentId), dto.amount, agentId, dto.requestId, dto.remark, ); return jsonResponse(result); } @Get('bets') async listBets( @CurrentUser('id') agentId: bigint, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { const p = Math.max(1, page ? parseInt(page, 10) : 1); const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100); const skip = (p - 1) * size; const agentIds = await this.agents.getSubtreeAgentIds(agentId); const [items, total] = await Promise.all([ this.prisma.bet.findMany({ where: { agentId: { in: agentIds } }, include: { selections: true, user: true }, orderBy: { placedAt: 'desc' }, skip, take: size, }), this.prisma.bet.count({ where: { agentId: { in: agentIds } } }), ]); return jsonResponse({ items, total, page: p, pageSize: size }); } @Get('reports/summary') async reportSummary(@CurrentUser('id') agentId: bigint) { const summary = await this.agents.getReportSummary(agentId); return jsonResponse(summary); } @Get('credit-transactions') async listCreditTransactions( @CurrentUser('id') agentId: bigint, @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('agentId') filterAgentId?: string, @Query('keyword') keyword?: string, @Query('operatorKeyword') operatorKeyword?: string, @Query('transactionType') transactionType?: string, @Query('dateFrom') dateFrom?: string, @Query('dateTo') dateTo?: string, ) { const scopedAgentIds = await this.agents.getSubtreeAgentIds(agentId); const parsedAgentId = filterAgentId ? BigInt(filterAgentId) : undefined; if (parsedAgentId && !scopedAgentIds.some((id) => id === parsedAgentId)) { return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 }); } const result = await this.agents.listCreditTransactions({ page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20, agentId: parsedAgentId, keyword, operatorKeyword, transactionType, scopedAgentIds, dateFrom: dateFrom ? new Date(dateFrom) : undefined, dateTo: dateTo ? new Date(dateTo) : undefined, }); return jsonResponse(result); } @Get('wallet-transactions') async walletTransactions( @CurrentUser('id') agentId: bigint, @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('playerId') playerId?: string, @Query('parentAgentId') parentAgentId?: string, @Query('parentAgentKeyword') parentAgentKeyword?: string, @Query('keyword') keyword?: string, @Query('operatorKeyword') operatorKeyword?: string, @Query('transactionType') transactionType?: string, @Query('dateFrom') dateFrom?: string, @Query('dateTo') dateTo?: string, ) { const scopedParentAgentIds = await this.agents.getSubtreeAgentIds(agentId); const parsedParentAgentId = parentAgentId ? BigInt(parentAgentId) : undefined; if ( parsedParentAgentId && !scopedParentAgentIds.some((id) => id === parsedParentAgentId) ) { return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 }); } if (playerId) { const player = await this.prisma.user.findFirst({ where: { id: BigInt(playerId), userType: 'PLAYER', deletedAt: null }, select: { id: true, parentId: true }, }); if ( !player?.parentId || !scopedParentAgentIds.some((id) => id === player.parentId) ) { return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 }); } } const result = await this.wallet.listTransferTransactions({ page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20, playerId: playerId ? BigInt(playerId) : undefined, parentAgentId: parsedParentAgentId, parentAgentKeyword, scopedParentAgentIds, keyword, operatorKeyword, transactionType, dateFrom: dateFrom ? new Date(dateFrom) : undefined, dateTo: dateTo ? new Date(dateTo) : undefined, }); return jsonResponse(result); } }