feat: 开户备注、账单展示优化与后台代理管理增强
- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示 - 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分 - 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端 - 后台代理/玩家表格操作栏重构,充值订单增加备注列 - 前台个人中心、充值、账单与验证码组件体验优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -556,6 +556,12 @@ class UpdatePlatformMatchDto {
|
||||
awayTeamLogoUrl?: string;
|
||||
}
|
||||
|
||||
class ReopenMatchDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
startTime?: string;
|
||||
}
|
||||
|
||||
class BatchMatchOddsDto {
|
||||
@IsArray()
|
||||
updates!: OutrightOddsUpdateItemDto[];
|
||||
@@ -1643,6 +1649,14 @@ export class AdminController {
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/:id/reopen')
|
||||
@RequirePermissions(P.matches)
|
||||
async reopenMatch(@Param('id') id: string, @Body() dto: ReopenMatchDto) {
|
||||
const startTime = dto.startTime ? new Date(dto.startTime) : undefined;
|
||||
const match = await this.matches.reopenMatch(BigInt(id), startTime);
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@Post('matches/:id/cancel')
|
||||
@RequirePermissions(P.matches)
|
||||
async cancelMatch(@Param('id') id: string) {
|
||||
|
||||
@@ -12,7 +12,8 @@ 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 { appForbidden } from '../../shared/common/app-error';
|
||||
import { appBadRequest, appForbidden } from '../../shared/common/app-error';
|
||||
import { validateInitialDepositRemark } from '@thebet365/shared';
|
||||
import { AgentsService } from '../../domains/agent/agents.service';
|
||||
import { WalletService } from '../../domains/ledger/wallet.service';
|
||||
import { BetsService } from '../../domains/betting/bets.service';
|
||||
@@ -164,7 +165,11 @@ export class AgentPortalController {
|
||||
const profile = await this.agents.getProfile(agentId);
|
||||
const maxLevel = await this.agents.getMaxAgentLevel();
|
||||
return jsonResponse({
|
||||
...profile,
|
||||
level: profile.level,
|
||||
creditLimit: profile.creditLimit.toString(),
|
||||
usedCredit: profile.usedCredit.toString(),
|
||||
availableCredit: profile.availableCredit.toString(),
|
||||
cashbackRate: profile.cashbackRate.toString(),
|
||||
maxAgentLevel: maxLevel,
|
||||
canManageSubAgents: this.agents.canCreateSubAgent(level, maxLevel),
|
||||
});
|
||||
@@ -176,6 +181,25 @@ export class AgentPortalController {
|
||||
return jsonResponse(players);
|
||||
}
|
||||
|
||||
@Get('players/scoped')
|
||||
async listScopedPlayers(
|
||||
@CurrentUser('id') agentId: bigint,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('parentAgentId') parentAgentId?: string,
|
||||
) {
|
||||
const result = await this.agents.listSubtreePlayersForPortal(agentId, {
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
keyword,
|
||||
status,
|
||||
parentAgentId,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('players')
|
||||
async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) {
|
||||
const user = await this.agents.createPlayer(agentId, {
|
||||
@@ -188,12 +212,14 @@ export class AgentPortalController {
|
||||
});
|
||||
|
||||
if (dto.initialDeposit != null && dto.initialDeposit > 0) {
|
||||
const remarkResult = validateInitialDepositRemark(dto.initialDeposit, dto.remark, 'agent');
|
||||
if (!remarkResult.ok) throw appBadRequest(remarkResult.code);
|
||||
await this.agents.depositToPlayer(
|
||||
agentId,
|
||||
user.id,
|
||||
dto.initialDeposit,
|
||||
`agent-create-${user.id}-${Date.now()}`,
|
||||
dto.remark ?? '开户初始余额',
|
||||
remarkResult.remark,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,6 +271,40 @@ export class AgentPortalController {
|
||||
return jsonResponse(agents);
|
||||
}
|
||||
|
||||
@Get('agents/level-counts')
|
||||
async subtreeAgentLevelCounts(@CurrentUser('id') agentId: bigint) {
|
||||
const counts = await this.agents.countSubtreeAgentsByLevel(agentId);
|
||||
return jsonResponse(counts);
|
||||
}
|
||||
|
||||
@Get('agents/options')
|
||||
async subtreeAgentOptions(@CurrentUser('id') agentId: bigint) {
|
||||
const options = await this.agents.listSubtreeAgentOptions(agentId);
|
||||
return jsonResponse(options);
|
||||
}
|
||||
|
||||
@Get('agents/by-level')
|
||||
async subtreeAgentsByLevel(
|
||||
@CurrentUser('id') agentId: bigint,
|
||||
@Query('level') level: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
const lvl = parseInt(level, 10);
|
||||
if (!Number.isFinite(lvl) || lvl < 1) {
|
||||
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
}
|
||||
const result = await this.agents.listSubtreeAgentsAtLevel(agentId, lvl, {
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
keyword,
|
||||
status,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('agents')
|
||||
async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) {
|
||||
const maxLevel = await this.agents.getMaxAgentLevel();
|
||||
@@ -323,6 +383,19 @@ export class AgentPortalController {
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Get('agents/:id/downline')
|
||||
async getSubAgentDownline(
|
||||
@CurrentUser('id') agentId: bigint,
|
||||
@CurrentUser('agentLevel') level: number,
|
||||
@Param('id') subAgentId: string,
|
||||
) {
|
||||
if (!(await this.canManageSubAgents(level))) {
|
||||
return jsonResponse({ agents: [], players: [] });
|
||||
}
|
||||
const downline = await this.agents.getDirectChildDownlineView(agentId, BigInt(subAgentId));
|
||||
return jsonResponse(downline);
|
||||
}
|
||||
|
||||
@Get('agents/:id/players')
|
||||
async listSubAgentPlayers(
|
||||
@CurrentUser('id') agentId: bigint,
|
||||
@@ -332,8 +405,8 @@ export class AgentPortalController {
|
||||
if (!(await this.canManageSubAgents(level))) {
|
||||
return jsonResponse([]);
|
||||
}
|
||||
await this.agents.assertDirectChildAgent(agentId, BigInt(subAgentId));
|
||||
const players = await this.agents.getDirectPlayers(BigInt(subAgentId));
|
||||
await this.agents.assertDescendantAgent(agentId, BigInt(subAgentId));
|
||||
const players = await this.agents.getPortalAgentDirectPlayers(agentId, BigInt(subAgentId));
|
||||
return jsonResponse(players);
|
||||
}
|
||||
|
||||
@@ -494,16 +567,7 @@ export class AgentPortalController {
|
||||
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 });
|
||||
}
|
||||
await this.agents.requirePlayerInPortalSubtree(agentId, BigInt(playerId));
|
||||
}
|
||||
const result = await this.wallet.listWalletTransactionsAdmin({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AuthService } from '../identity/auth.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { generateBatchNo } from '../../shared/common/decorators';
|
||||
import { assertPlayerUsername } from '@thebet365/shared';
|
||||
import { assertPlayerUsername, validateInitialDepositRemark } from '@thebet365/shared';
|
||||
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
|
||||
import { assignInviteCodeWithHistory } from '../../shared/common/invite-code.util';
|
||||
|
||||
@@ -87,6 +87,157 @@ export class AgentsService {
|
||||
return map;
|
||||
}
|
||||
|
||||
private async agentCashbackRateMap(agentUserIds: bigint[]): Promise<Map<string, string>> {
|
||||
if (agentUserIds.length === 0) return new Map();
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: agentUserIds } },
|
||||
select: { userId: true, cashbackRate: true },
|
||||
});
|
||||
return new Map(profiles.map((p) => [p.userId.toString(), p.cashbackRate.toString()]));
|
||||
}
|
||||
|
||||
private async playerEffectiveCashbackRateMap(
|
||||
players: Array<{ id: bigint; parentId: bigint | null }>,
|
||||
parentCashbackMap: Map<string, string>,
|
||||
): Promise<Map<string, string>> {
|
||||
if (players.length === 0) return new Map();
|
||||
const playerIds = players.map((p) => p.id);
|
||||
const customRules = await this.prisma.cashbackRule.findMany({
|
||||
where: {
|
||||
targetType: 'USER',
|
||||
targetId: { in: playerIds },
|
||||
isActive: true,
|
||||
marketType: null,
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
const customMap = new Map<string, string>();
|
||||
for (const rule of customRules) {
|
||||
if (!rule.targetId) continue;
|
||||
const id = rule.targetId.toString();
|
||||
if (!customMap.has(id) && new Decimal(rule.rate).gt(0)) {
|
||||
customMap.set(id, rule.rate.toString());
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Map<string, string>();
|
||||
for (const p of players) {
|
||||
const key = p.id.toString();
|
||||
const custom = customMap.get(key);
|
||||
if (custom) {
|
||||
result.set(key, custom);
|
||||
continue;
|
||||
}
|
||||
if (p.parentId) {
|
||||
result.set(key, parentCashbackMap.get(p.parentId.toString()) ?? '0');
|
||||
} else {
|
||||
result.set(key, '0');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 代理端:从当前登录代理向下构建上级链,不包含更上层代理 */
|
||||
private async buildScopedAncestorChainMap(
|
||||
parentAgentIds: (bigint | null | undefined)[],
|
||||
rootAgentId: bigint,
|
||||
) {
|
||||
const cache = new Map<string, { username: string; parentAgentId: bigint | null }>();
|
||||
const pending = new Set<bigint>();
|
||||
const rootKey = rootAgentId.toString();
|
||||
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) pending.add(id);
|
||||
}
|
||||
pending.add(rootAgentId);
|
||||
|
||||
while (pending.size > 0) {
|
||||
const batch = [...pending];
|
||||
pending.clear();
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: batch } },
|
||||
select: {
|
||||
userId: true,
|
||||
parentAgentId: true,
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
});
|
||||
for (const profile of profiles) {
|
||||
cache.set(profile.userId.toString(), {
|
||||
username: profile.user.username,
|
||||
parentAgentId: profile.parentAgentId,
|
||||
});
|
||||
if (
|
||||
profile.parentAgentId &&
|
||||
profile.parentAgentId.toString() !== rootKey &&
|
||||
!cache.has(profile.parentAgentId.toString())
|
||||
) {
|
||||
pending.add(profile.parentAgentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const build = (startId: bigint | null | undefined): string[] => {
|
||||
if (!startId) return [];
|
||||
const chain: string[] = [];
|
||||
let cur: bigint | null = startId;
|
||||
let reachedRoot = false;
|
||||
while (cur) {
|
||||
const hit = cache.get(cur.toString());
|
||||
if (!hit) return [];
|
||||
chain.unshift(hit.username);
|
||||
if (cur.toString() === rootKey) {
|
||||
reachedRoot = true;
|
||||
break;
|
||||
}
|
||||
cur = hit.parentAgentId;
|
||||
}
|
||||
return reachedRoot ? chain : [];
|
||||
};
|
||||
|
||||
const map = new Map<string, string[]>();
|
||||
for (const id of parentAgentIds) {
|
||||
if (id) map.set(id.toString(), build(id));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private async getAgentPortalScope(rootAgentId: bigint) {
|
||||
const profile = await this.getProfile(rootAgentId);
|
||||
const subtreeIds = await this.getSubtreeAgentIds(rootAgentId);
|
||||
const descendantIds = subtreeIds.filter((id) => id !== rootAgentId);
|
||||
const subtreeIdSet = new Set(subtreeIds.map((id) => id.toString()));
|
||||
return {
|
||||
rootAgentId,
|
||||
rootLevel: profile.level,
|
||||
subtreeIds,
|
||||
descendantIds,
|
||||
subtreeIdSet,
|
||||
};
|
||||
}
|
||||
|
||||
private assertAgentInPortalSubtree(
|
||||
scope: { subtreeIdSet: Set<string> },
|
||||
agentId: bigint,
|
||||
) {
|
||||
if (!scope.subtreeIdSet.has(agentId.toString())) {
|
||||
throw appForbidden('NOT_SUB_AGENT');
|
||||
}
|
||||
}
|
||||
|
||||
/** 代理端:玩家必须挂在当前代理子树内某代理下(自己或下级) */
|
||||
async requirePlayerInPortalSubtree(rootAgentId: bigint, playerId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const player = await this.prisma.user.findFirst({
|
||||
where: { id: playerId, userType: 'PLAYER', deletedAt: null },
|
||||
include: { wallet: true, preferences: true, auth: true },
|
||||
});
|
||||
if (!player?.parentId || !scope.subtreeIdSet.has(player.parentId.toString())) {
|
||||
throw appForbidden('MANAGE_DIRECT_PLAYERS_ONLY');
|
||||
}
|
||||
return { player, scope, isDirect: player.parentId.toString() === rootAgentId.toString() };
|
||||
}
|
||||
|
||||
private async validateAgentLevel(level: number, parentAgentId?: bigint) {
|
||||
if (!Number.isInteger(level) || level < 1) {
|
||||
throw appBadRequest('AGENT_LEVEL_INVALID');
|
||||
@@ -341,6 +492,7 @@ export class AgentsService {
|
||||
operatorId,
|
||||
remark ?? '管理员上分',
|
||||
requestId,
|
||||
'ADMIN_DEPOSIT',
|
||||
);
|
||||
const player = await this.prisma.user.findUnique({
|
||||
where: { id: playerId },
|
||||
@@ -366,6 +518,7 @@ export class AgentsService {
|
||||
operatorId,
|
||||
remark ?? '管理员下分',
|
||||
requestId,
|
||||
'ADMIN_WITHDRAW',
|
||||
);
|
||||
const player = await this.prisma.user.findUnique({
|
||||
where: { id: playerId },
|
||||
@@ -519,7 +672,7 @@ export class AgentsService {
|
||||
|
||||
await this.assertAgentDepositLimits(agentId, amt);
|
||||
|
||||
await this.wallet.deposit(playerId, amt, agentId, remark ?? '代理上分', requestId);
|
||||
await this.wallet.deposit(playerId, amt, agentId, remark ?? '代理上分', requestId, 'AGENT_DEPOSIT');
|
||||
await this.recalculateUsedCredit(agentId);
|
||||
|
||||
return { success: true };
|
||||
@@ -534,7 +687,14 @@ export class AgentsService {
|
||||
) {
|
||||
await this.requireDirectPlayer(agentId, playerId);
|
||||
|
||||
await this.wallet.withdraw(playerId, amount, agentId, remark ?? '代理下分', requestId);
|
||||
await this.wallet.withdraw(
|
||||
playerId,
|
||||
amount,
|
||||
agentId,
|
||||
remark ?? '代理下分',
|
||||
requestId,
|
||||
'AGENT_WITHDRAW',
|
||||
);
|
||||
await this.recalculateUsedCredit(agentId);
|
||||
|
||||
return { success: true };
|
||||
@@ -1515,6 +1675,8 @@ export class AgentsService {
|
||||
|
||||
const initial = data.initialDeposit ?? 0;
|
||||
if (initial > 0) {
|
||||
const remarkResult = validateInitialDepositRemark(initial, data.depositRemark, 'admin');
|
||||
if (!remarkResult.ok) throw appBadRequest(remarkResult.code);
|
||||
const requestId =
|
||||
data.depositRequestId ?? `admin-create-${user.id}-${Date.now()}`;
|
||||
await this.assertPlayerParentCreditForDeposit(user.id, initial);
|
||||
@@ -1522,8 +1684,9 @@ export class AgentsService {
|
||||
user.id,
|
||||
initial,
|
||||
operatorId,
|
||||
data.depositRemark ?? '开户初始余额',
|
||||
remarkResult.remark,
|
||||
requestId,
|
||||
'ADMIN_DEPOSIT',
|
||||
);
|
||||
if (parentId) {
|
||||
await this.recalculateUsedCredit(parentId);
|
||||
@@ -1533,6 +1696,34 @@ export class AgentsService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async getPortalAgentDirectPlayers(rootAgentId: bigint, targetAgentId: bigint) {
|
||||
await this.assertDescendantAgent(rootAgentId, targetAgentId);
|
||||
const players = await this.getDirectPlayers(targetAgentId);
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: targetAgentId },
|
||||
select: {
|
||||
cashbackRate: true,
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
});
|
||||
const rootKey = rootAgentId.toString();
|
||||
const targetKey = targetAgentId.toString();
|
||||
const parentAgentUsername = profile?.user.username ?? '—';
|
||||
const parentCashbackMap = await this.agentCashbackRateMap([targetAgentId]);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
players.map((p) => ({ id: BigInt(p.id), parentId: targetAgentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
return players.map((p) => ({
|
||||
...p,
|
||||
parentAgentId: targetKey,
|
||||
parentAgentUsername,
|
||||
cashbackRate: playerCashbackMap.get(p.id) ?? '0',
|
||||
inChain: true,
|
||||
isDirect: targetKey === rootKey,
|
||||
}));
|
||||
}
|
||||
|
||||
async getDirectPlayers(agentId: bigint) {
|
||||
const rows = await this.prisma.user.findMany({
|
||||
where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
|
||||
@@ -1592,6 +1783,8 @@ export class AgentsService {
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentAgentId: p.parentAgentId?.toString() ?? null,
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
@@ -1601,6 +1794,403 @@ export class AgentsService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Read-only downline under a direct sub-agent (all descendant agents + subtree players). */
|
||||
async getDirectChildDownlineView(parentAgentId: bigint, subAgentId: bigint) {
|
||||
await this.assertDirectChildAgent(parentAgentId, subAgentId);
|
||||
|
||||
const subtreeIds = await this.getSubtreeAgentIds(subAgentId);
|
||||
const descendantAgentIds = subtreeIds.filter((id) => id !== subAgentId);
|
||||
|
||||
let agents: Array<{
|
||||
userId: string;
|
||||
username: string;
|
||||
userStatus: string;
|
||||
status: string;
|
||||
level: number;
|
||||
parentUsername: string;
|
||||
creditLimit: string;
|
||||
usedCredit: string;
|
||||
availableCredit: string;
|
||||
directPlayerCount: number;
|
||||
createdAt: Date;
|
||||
}> = [];
|
||||
|
||||
if (descendantAgentIds.length > 0) {
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: { userId: { in: descendantAgentIds } },
|
||||
include: { user: true },
|
||||
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
const parentAgentIds = [
|
||||
...new Set(
|
||||
profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentUsers =
|
||||
parentAgentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentAgentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentNameMap = new Map(
|
||||
parentUsers.map((u) => [u.id.toString(), u.username]),
|
||||
);
|
||||
|
||||
const playerCounts = await this.prisma.user.groupBy({
|
||||
by: ['parentId'],
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
parentId: { in: descendantAgentIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const countMap = new Map(
|
||||
playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
|
||||
);
|
||||
|
||||
agents = profiles.map((p) => {
|
||||
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
|
||||
return {
|
||||
userId: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentUsername: p.parentAgentId
|
||||
? parentNameMap.get(p.parentAgentId.toString()) ?? '—'
|
||||
: '—',
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
|
||||
createdAt: p.createdAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const playerRows =
|
||||
subtreeIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
parentId: { in: subtreeIds },
|
||||
},
|
||||
include: {
|
||||
wallet: true,
|
||||
usedInvite: { select: { code: true } },
|
||||
parent: { select: { username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
: [];
|
||||
|
||||
const parentAgentIdsForPlayers = [
|
||||
...new Set(
|
||||
playerRows
|
||||
.map((u) => u.parentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentCashbackMap = await this.agentCashbackRateMap(parentAgentIdsForPlayers);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
playerRows.map((u) => ({ id: u.id, parentId: u.parentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
|
||||
const players = playerRows.map((u) => ({
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
status: u.status,
|
||||
createdAt: u.createdAt,
|
||||
inviteCode: u.usedInvite?.code ?? null,
|
||||
parentAgentUsername: u.parent?.username ?? '—',
|
||||
cashbackRate: playerCashbackMap.get(u.id.toString()) ?? '0',
|
||||
wallet: u.wallet
|
||||
? {
|
||||
availableBalance: u.wallet.availableBalance.toString(),
|
||||
frozenBalance: u.wallet.frozenBalance.toString(),
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
agents: agents.map((a) => ({
|
||||
...a,
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
})),
|
||||
players,
|
||||
};
|
||||
}
|
||||
|
||||
async assertDescendantAgent(rootAgentId: bigint, targetAgentId: bigint) {
|
||||
const subtreeIds = await this.getSubtreeAgentIds(rootAgentId);
|
||||
if (!subtreeIds.some((id) => id === targetAgentId)) {
|
||||
throw appForbidden('NOT_SUB_AGENT');
|
||||
}
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: targetAgentId },
|
||||
});
|
||||
if (!profile) throw appNotFound('AGENT_NOT_FOUND');
|
||||
return profile;
|
||||
}
|
||||
|
||||
async countSubtreeAgentsByLevel(rootAgentId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
if (scope.descendantIds.length === 0) return {} as Record<number, number>;
|
||||
|
||||
const groups = await this.prisma.agentProfile.groupBy({
|
||||
by: ['level'],
|
||||
where: {
|
||||
userId: { in: scope.descendantIds },
|
||||
level: { gt: scope.rootLevel },
|
||||
user: { deletedAt: null },
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const out: Record<number, number> = {};
|
||||
for (const g of groups) {
|
||||
if (g.level > scope.rootLevel) {
|
||||
out[g.level] = g._count._all;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async listSubtreeAgentsAtLevel(
|
||||
rootAgentId: bigint,
|
||||
level: number,
|
||||
params?: { page?: number; pageSize?: number; keyword?: string; status?: string },
|
||||
) {
|
||||
const page = Math.max(1, params?.page ?? 1);
|
||||
const pageSize = Math.min(Math.max(1, params?.pageSize ?? 20), 100);
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
if (level <= scope.rootLevel || scope.descendantIds.length === 0) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
|
||||
const where: Prisma.AgentProfileWhereInput = {
|
||||
userId: { in: scope.descendantIds },
|
||||
level,
|
||||
};
|
||||
const kw = params?.keyword?.trim();
|
||||
const status = params?.status?.trim();
|
||||
const userWhere: Prisma.UserWhereInput = { deletedAt: null };
|
||||
if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
|
||||
userWhere.status = status;
|
||||
}
|
||||
if (kw) {
|
||||
userWhere.username = { contains: kw, mode: 'insensitive' };
|
||||
}
|
||||
where.user = userWhere;
|
||||
|
||||
const [profiles, total] = await Promise.all([
|
||||
this.prisma.agentProfile.findMany({
|
||||
where,
|
||||
include: { user: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.agentProfile.count({ where }),
|
||||
]);
|
||||
|
||||
const agentIds = profiles.map((p) => p.userId);
|
||||
const playerCounts =
|
||||
agentIds.length > 0
|
||||
? await this.prisma.user.groupBy({
|
||||
by: ['parentId'],
|
||||
where: {
|
||||
userType: 'PLAYER',
|
||||
parentId: { in: agentIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
_count: { _all: true },
|
||||
})
|
||||
: [];
|
||||
const countMap = new Map(
|
||||
playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
|
||||
);
|
||||
|
||||
const parentAgentIds = [
|
||||
...new Set(
|
||||
profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null && scope.subtreeIdSet.has(id.toString())),
|
||||
),
|
||||
];
|
||||
const parentUsers =
|
||||
parentAgentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentAgentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
|
||||
const parentChainMap = await this.buildScopedAncestorChainMap(
|
||||
profiles.map((p) => p.parentAgentId),
|
||||
rootAgentId,
|
||||
);
|
||||
|
||||
const items = profiles.map((p) => {
|
||||
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
|
||||
const parentChain = p.parentAgentId
|
||||
? (parentChainMap.get(p.parentAgentId.toString()) ?? [])
|
||||
: [];
|
||||
const parentUsername =
|
||||
p.parentAgentId && scope.subtreeIdSet.has(p.parentAgentId.toString())
|
||||
? (parentUsernameMap.get(p.parentAgentId.toString()) ?? null)
|
||||
: null;
|
||||
return {
|
||||
userId: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
userStatus: p.user.status,
|
||||
status: p.status,
|
||||
level: p.level,
|
||||
parentAgentId: p.parentAgentId?.toString() ?? null,
|
||||
parentUsername,
|
||||
parentChainLabel: parentChain.length ? parentChain.join(' / ') : null,
|
||||
cashbackRate: p.cashbackRate.toString(),
|
||||
creditLimit: dec(p.creditLimit),
|
||||
usedCredit: dec(p.usedCredit),
|
||||
availableCredit: available.toString(),
|
||||
directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
|
||||
createdAt: p.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async listSubtreePlayersForPortal(
|
||||
rootAgentId: bigint,
|
||||
params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
parentAgentId?: string;
|
||||
},
|
||||
) {
|
||||
const page = Math.max(1, params?.page ?? 1);
|
||||
const pageSize = Math.min(Math.max(1, params?.pageSize ?? 20), 100);
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const where: Prisma.UserWhereInput = {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
parentId: { in: scope.subtreeIds },
|
||||
};
|
||||
if (params?.parentAgentId) {
|
||||
this.assertAgentInPortalSubtree(scope, BigInt(params.parentAgentId));
|
||||
where.parentId = BigInt(params.parentAgentId);
|
||||
}
|
||||
const kw = params?.keyword?.trim();
|
||||
if (kw) {
|
||||
where.username = { contains: kw, mode: 'insensitive' };
|
||||
}
|
||||
const status = params?.status?.trim();
|
||||
if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.user.findMany({
|
||||
where,
|
||||
include: {
|
||||
wallet: true,
|
||||
usedInvite: { select: { code: true } },
|
||||
parent: { select: { id: true, username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
const parentIds = [
|
||||
...new Set(
|
||||
rows
|
||||
.map((u) => u.parentId)
|
||||
.filter((id): id is bigint => id != null),
|
||||
),
|
||||
];
|
||||
const parentCashbackMap = await this.agentCashbackRateMap(parentIds);
|
||||
const playerCashbackMap = await this.playerEffectiveCashbackRateMap(
|
||||
rows.map((u) => ({ id: u.id, parentId: u.parentId })),
|
||||
parentCashbackMap,
|
||||
);
|
||||
|
||||
const rootKey = rootAgentId.toString();
|
||||
const items = rows.map((u) => {
|
||||
const parentId = u.parentId!.toString();
|
||||
return {
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
status: u.status,
|
||||
createdAt: u.createdAt.toISOString(),
|
||||
inviteCode: u.usedInvite?.code ?? null,
|
||||
parentAgentId: parentId,
|
||||
parentAgentUsername: u.parent?.username ?? '—',
|
||||
cashbackRate: playerCashbackMap.get(u.id.toString()) ?? '0',
|
||||
inChain: true,
|
||||
isDirect: parentId === rootKey,
|
||||
wallet: u.wallet
|
||||
? {
|
||||
availableBalance: u.wallet.availableBalance.toString(),
|
||||
frozenBalance: u.wallet.frozenBalance.toString(),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async listSubtreeAgentOptions(rootAgentId: bigint) {
|
||||
const scope = await this.getAgentPortalScope(rootAgentId);
|
||||
const profiles = await this.prisma.agentProfile.findMany({
|
||||
where: {
|
||||
userId: { in: scope.subtreeIds },
|
||||
OR: [{ userId: rootAgentId }, { level: { gt: scope.rootLevel } }],
|
||||
},
|
||||
include: { user: { select: { username: true } } },
|
||||
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
const parentIds = profiles
|
||||
.map((p) => p.parentAgentId)
|
||||
.filter((id): id is bigint => id != null && scope.subtreeIdSet.has(id.toString()));
|
||||
const parentUsers =
|
||||
parentIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: [...new Set(parentIds)] } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
|
||||
|
||||
return profiles.map((p) => ({
|
||||
id: p.userId.toString(),
|
||||
username: p.user.username,
|
||||
level: p.level,
|
||||
parentUsername:
|
||||
p.parentAgentId && scope.subtreeIdSet.has(p.parentAgentId.toString())
|
||||
? (parentUsernameMap.get(p.parentAgentId.toString()) ?? null)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
async assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) {
|
||||
const profile = await this.prisma.agentProfile.findUnique({
|
||||
where: { userId: subAgentId },
|
||||
|
||||
@@ -1101,6 +1101,29 @@ export class MatchesService {
|
||||
});
|
||||
}
|
||||
|
||||
async reopenMatch(matchId: bigint, startTime?: Date) {
|
||||
const match = await this.requireAdminMatch(matchId);
|
||||
if (match.isOutright) throw appBadRequest('OUTRIGHT_EDIT_VIA_MARKETS');
|
||||
if (match.status !== 'CLOSED') throw appBadRequest('MATCH_NOT_REOPENABLE');
|
||||
|
||||
const scoreRow = await this.prisma.matchScore.findUnique({ where: { matchId } });
|
||||
if (scoreRow) throw appBadRequest('MATCH_NOT_REOPENABLE');
|
||||
|
||||
const effectiveStart = startTime ?? match.startTime;
|
||||
if (!isPreMatchKickoff(effectiveStart)) {
|
||||
throw appBadRequest('MATCH_REOPEN_KICKOFF_REQUIRED');
|
||||
}
|
||||
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
data: {
|
||||
status: 'PUBLISHED',
|
||||
closeTime: null,
|
||||
...(startTime ? { startTime } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async cancelMatch(matchId: bigint) {
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
|
||||
@@ -432,6 +432,7 @@ export class DepositService {
|
||||
reviewerId: operatorId,
|
||||
reviewedAt: new Date(),
|
||||
rejectReason: reason,
|
||||
remark: reason,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ export class WalletService {
|
||||
operatorId: bigint,
|
||||
remark?: string,
|
||||
referenceId?: string,
|
||||
transactionType = 'MANUAL_WITHDRAW',
|
||||
) {
|
||||
const amt = new Decimal(amount);
|
||||
if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');
|
||||
@@ -106,7 +107,7 @@ export class WalletService {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: 'MANUAL_WITHDRAW',
|
||||
transactionType,
|
||||
amount: amt.neg(),
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
@@ -260,26 +261,178 @@ export class WalletService {
|
||||
return this.prisma.$transaction(run);
|
||||
}
|
||||
|
||||
private static readonly DEPOSIT_TX_TYPES = [
|
||||
'MANUAL_DEPOSIT',
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'INITIAL_DEPOSIT',
|
||||
'DEPOSIT',
|
||||
'MANUAL_ADJUST',
|
||||
'PLAYER_DEPOSIT',
|
||||
] as const;
|
||||
|
||||
private static readonly WITHDRAW_TX_TYPES = [
|
||||
'MANUAL_WITHDRAW',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_WITHDRAW',
|
||||
'WITHDRAW',
|
||||
] as const;
|
||||
|
||||
private static readonly SYSTEM_REMARKS = new Set([
|
||||
'管理员上分',
|
||||
'管理员下分',
|
||||
'代理上分',
|
||||
'代理下分',
|
||||
'开户初始余额',
|
||||
'Resettlement adjustment',
|
||||
]);
|
||||
|
||||
private resolveDisplayType(
|
||||
transactionType: string,
|
||||
operatorType?: string | null,
|
||||
): string {
|
||||
const type = transactionType.toUpperCase();
|
||||
if (type === 'INITIAL_DEPOSIT') {
|
||||
if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
|
||||
return 'ADMIN_DEPOSIT';
|
||||
}
|
||||
if (
|
||||
[
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_WITHDRAW',
|
||||
'PLAYER_DEPOSIT',
|
||||
].includes(type)
|
||||
) {
|
||||
return type;
|
||||
}
|
||||
if (type === 'MANUAL_DEPOSIT') {
|
||||
if (operatorType === 'ADMIN') return 'ADMIN_DEPOSIT';
|
||||
if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
|
||||
return type;
|
||||
}
|
||||
if (type === 'MANUAL_WITHDRAW') {
|
||||
if (operatorType === 'ADMIN') return 'ADMIN_WITHDRAW';
|
||||
if (operatorType === 'AGENT') return 'AGENT_WITHDRAW';
|
||||
return type;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private isCustomRemark(remark: string | null | undefined): boolean {
|
||||
const r = remark?.trim();
|
||||
if (!r) return false;
|
||||
if (WalletService.SYSTEM_REMARKS.has(r)) return false;
|
||||
if (r.startsWith('Cashback batch ')) return false;
|
||||
if (r.startsWith('Deposit order ')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private buildPlayerTxSummary(
|
||||
tx: {
|
||||
transactionType: string;
|
||||
referenceType: string | null;
|
||||
referenceId: string | null;
|
||||
remark: string | null;
|
||||
},
|
||||
depositMethodName?: string | null,
|
||||
): string | null {
|
||||
const type = tx.transactionType.toUpperCase();
|
||||
if (tx.referenceType === 'BET' && tx.referenceId) {
|
||||
return tx.referenceId;
|
||||
}
|
||||
if (type === 'CASHBACK' || type === 'CASHBACK_DEPOSIT') {
|
||||
return tx.referenceId ?? null;
|
||||
}
|
||||
if (type === 'PLAYER_DEPOSIT') {
|
||||
const parts = [depositMethodName?.trim(), tx.referenceId?.trim()].filter(Boolean);
|
||||
return parts.length ? parts.join(' · ') : null;
|
||||
}
|
||||
if (this.isCustomRemark(tx.remark)) return tx.remark!.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveSummaryKind(remark: string | null | undefined): 'opening_bonus' | null {
|
||||
const r = remark?.trim();
|
||||
if (r === '开户初始余额') return 'opening_bonus';
|
||||
return null;
|
||||
}
|
||||
|
||||
private async enrichPlayerTransactions(
|
||||
rows: Array<{
|
||||
id: bigint;
|
||||
transactionId: string;
|
||||
transactionType: string;
|
||||
amount: Decimal;
|
||||
balanceBefore: Decimal;
|
||||
balanceAfter: Decimal;
|
||||
frozenBefore: Decimal;
|
||||
frozenAfter: Decimal;
|
||||
referenceType: string | null;
|
||||
referenceId: string | null;
|
||||
remark: string | null;
|
||||
operatorId: bigint | null;
|
||||
createdAt: Date;
|
||||
}>,
|
||||
) {
|
||||
const operatorIds = [
|
||||
...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
const operators =
|
||||
operatorIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, userType: true },
|
||||
})
|
||||
: [];
|
||||
const operatorTypeById = new Map(operators.map((o) => [o.id.toString(), o.userType]));
|
||||
|
||||
const depositMethodByRowId = await this.resolveDepositMethodsForRows(rows);
|
||||
|
||||
return rows.map((row) => {
|
||||
const operatorType = row.operatorId
|
||||
? operatorTypeById.get(row.operatorId.toString())
|
||||
: null;
|
||||
const displayType = this.resolveDisplayType(row.transactionType, operatorType);
|
||||
const depositMethod = depositMethodByRowId.get(row.id.toString());
|
||||
const summary = this.buildPlayerTxSummary(
|
||||
row,
|
||||
depositMethod?.depositMethodName,
|
||||
);
|
||||
const summaryKind = summary ? null : this.resolveSummaryKind(row.remark);
|
||||
return {
|
||||
transactionId: row.transactionId,
|
||||
transactionType: row.transactionType,
|
||||
displayType,
|
||||
summaryKind,
|
||||
amount: row.amount.toString(),
|
||||
balanceBefore: row.balanceBefore.toString(),
|
||||
balanceAfter: row.balanceAfter.toString(),
|
||||
frozenBefore: row.frozenBefore.toString(),
|
||||
frozenAfter: row.frozenAfter.toString(),
|
||||
referenceType: row.referenceType,
|
||||
referenceId: row.referenceId,
|
||||
remark: row.remark,
|
||||
summary,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
betNo: row.referenceType === 'BET' ? row.referenceId : null,
|
||||
cashbackBatchNo:
|
||||
row.transactionType === 'CASHBACK' || row.transactionType === 'CASHBACK_DEPOSIT'
|
||||
? row.referenceId
|
||||
: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getTransactionDetail(userId: bigint, transactionId: string) {
|
||||
const tx = await this.prisma.walletTransaction.findFirst({
|
||||
where: { userId, transactionId },
|
||||
});
|
||||
if (!tx) return null;
|
||||
|
||||
return {
|
||||
transactionId: tx.transactionId,
|
||||
transactionType: tx.transactionType,
|
||||
amount: tx.amount.toString(),
|
||||
balanceBefore: tx.balanceBefore.toString(),
|
||||
balanceAfter: tx.balanceAfter.toString(),
|
||||
frozenBefore: tx.frozenBefore.toString(),
|
||||
frozenAfter: tx.frozenAfter.toString(),
|
||||
referenceType: tx.referenceType,
|
||||
referenceId: tx.referenceId,
|
||||
remark: tx.remark,
|
||||
createdAt: tx.createdAt.toISOString(),
|
||||
betNo: tx.referenceType === 'BET' ? tx.referenceId : null,
|
||||
};
|
||||
const [enriched] = await this.enrichPlayerTransactions([tx]);
|
||||
return enriched;
|
||||
}
|
||||
|
||||
async getTransactions(userId: bigint, page = 1, pageSize = 20, typeFilter?: string) {
|
||||
@@ -287,9 +440,9 @@ export class WalletService {
|
||||
|
||||
let typeWhere: Record<string, unknown> = {};
|
||||
if (typeFilter === 'deposit') {
|
||||
typeWhere = { transactionType: { in: ['MANUAL_DEPOSIT', 'DEPOSIT', 'MANUAL_ADJUST', 'PLAYER_DEPOSIT'] } };
|
||||
typeWhere = { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
|
||||
} else if (typeFilter === 'withdraw') {
|
||||
typeWhere = { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
typeWhere = { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
|
||||
} else if (typeFilter === 'bet') {
|
||||
typeWhere = { transactionType: { in: ['BET_FREEZE', 'BET_DEDUCT', 'BET_SETTLE_WIN', 'BET_SETTLE_LOSE', 'BET_SETTLE_PUSH', 'BET_WIN', 'BET_REFUND', 'BET_VOID', 'BET_VOID_REFUND', 'RESETTLE_REVERSE'] } };
|
||||
} else if (typeFilter === 'cashback') {
|
||||
@@ -306,16 +459,21 @@ export class WalletService {
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
return {
|
||||
items: await this.enrichPlayerTransactions(items),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
private walletTypeCategoryWhere(category?: string): Prisma.WalletTransactionWhereInput {
|
||||
const cat = category?.trim();
|
||||
if (cat === 'deposit') {
|
||||
return { transactionType: { in: ['MANUAL_DEPOSIT', 'DEPOSIT', 'MANUAL_ADJUST', 'PLAYER_DEPOSIT'] } };
|
||||
return { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
|
||||
}
|
||||
if (cat === 'withdraw') {
|
||||
return { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
return { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
|
||||
}
|
||||
if (cat === 'bet') {
|
||||
return {
|
||||
@@ -343,6 +501,9 @@ export class WalletService {
|
||||
|
||||
private static readonly DEPOSIT_RECHARGE_TYPES = new Set([
|
||||
'MANUAL_DEPOSIT',
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'INITIAL_DEPOSIT',
|
||||
'DEPOSIT',
|
||||
'PLAYER_DEPOSIT',
|
||||
]);
|
||||
@@ -644,7 +805,15 @@ export class WalletService {
|
||||
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const transferTypes = ['MANUAL_DEPOSIT', 'MANUAL_WITHDRAW'];
|
||||
const transferTypes = [
|
||||
'MANUAL_DEPOSIT',
|
||||
'MANUAL_WITHDRAW',
|
||||
'ADMIN_DEPOSIT',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_DEPOSIT',
|
||||
'AGENT_WITHDRAW',
|
||||
'INITIAL_DEPOSIT',
|
||||
];
|
||||
const where: Prisma.WalletTransactionWhereInput = {
|
||||
transactionType: params.transactionType?.trim()
|
||||
? params.transactionType.trim()
|
||||
|
||||
Reference in New Issue
Block a user