feat: multi-tier agent hierarchy, wallet ledger, and player UX polish
Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -309,6 +309,227 @@ export class WalletService {
|
||||
return { 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'] } };
|
||||
}
|
||||
if (cat === 'withdraw') {
|
||||
return { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
}
|
||||
if (cat === 'bet') {
|
||||
return {
|
||||
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',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (cat === 'cashback') {
|
||||
return { transactionType: { in: ['CASHBACK', 'CASHBACK_DEPOSIT'] } };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async listWalletTransactionsAdmin(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
playerId?: bigint;
|
||||
parentAgentId?: bigint;
|
||||
parentAgentKeyword?: string;
|
||||
scopedParentAgentIds?: bigint[];
|
||||
keyword?: string;
|
||||
operatorKeyword?: string;
|
||||
transactionType?: string;
|
||||
typeCategory?: string;
|
||||
dateFrom?: Date;
|
||||
dateTo?: Date;
|
||||
}) {
|
||||
const page = Math.max(1, params.page ?? 1);
|
||||
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: Prisma.WalletTransactionWhereInput = {};
|
||||
|
||||
const explicitType = params.transactionType?.trim();
|
||||
if (explicitType) {
|
||||
where.transactionType = explicitType;
|
||||
} else if (params.typeCategory?.trim()) {
|
||||
Object.assign(where, this.walletTypeCategoryWhere(params.typeCategory));
|
||||
}
|
||||
|
||||
if (params.dateFrom || params.dateTo) {
|
||||
where.createdAt = {};
|
||||
if (params.dateFrom) where.createdAt.gte = params.dateFrom;
|
||||
if (params.dateTo) where.createdAt.lte = params.dateTo;
|
||||
}
|
||||
|
||||
const operatorKeyword = params.operatorKeyword?.trim();
|
||||
if (operatorKeyword) {
|
||||
const matchedOps = await this.prisma.user.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
username: { contains: operatorKeyword, mode: 'insensitive' },
|
||||
},
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const operatorIds = matchedOps.map((u) => u.id);
|
||||
if (!operatorIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
where.operatorId = { in: operatorIds };
|
||||
}
|
||||
|
||||
let playerIds: bigint[] | undefined;
|
||||
|
||||
if (params.playerId) {
|
||||
playerIds = [params.playerId];
|
||||
} else {
|
||||
const playerWhere: Prisma.UserWhereInput = {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (params.parentAgentId) {
|
||||
playerWhere.parentId = params.parentAgentId;
|
||||
} else if (params.parentAgentKeyword?.trim()) {
|
||||
const matchedAgents = await this.prisma.user.findMany({
|
||||
where: {
|
||||
userType: 'AGENT',
|
||||
deletedAt: null,
|
||||
username: { contains: params.parentAgentKeyword.trim(), mode: 'insensitive' },
|
||||
...(params.scopedParentAgentIds?.length
|
||||
? { id: { in: params.scopedParentAgentIds } }
|
||||
: {}),
|
||||
},
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const agentIds = matchedAgents.map((a) => a.id);
|
||||
if (!agentIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
playerWhere.parentId = { in: agentIds };
|
||||
} else if (params.scopedParentAgentIds?.length) {
|
||||
playerWhere.parentId = { in: params.scopedParentAgentIds };
|
||||
}
|
||||
|
||||
const keyword = params.keyword?.trim();
|
||||
if (keyword) {
|
||||
playerWhere.username = { contains: keyword, mode: 'insensitive' };
|
||||
}
|
||||
|
||||
if (
|
||||
params.parentAgentId ||
|
||||
params.parentAgentKeyword?.trim() ||
|
||||
params.scopedParentAgentIds?.length ||
|
||||
keyword
|
||||
) {
|
||||
const players = await this.prisma.user.findMany({
|
||||
where: playerWhere,
|
||||
select: { id: true },
|
||||
take: 500,
|
||||
});
|
||||
playerIds = players.map((p) => p.id);
|
||||
if (!playerIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playerIds) {
|
||||
where.userId = { in: playerIds };
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.walletTransaction.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where }),
|
||||
]);
|
||||
|
||||
const userIds = [...new Set(rows.map((r) => r.userId))];
|
||||
const operatorIds = [
|
||||
...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
|
||||
const [players, operators] = await Promise.all([
|
||||
userIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true, parentId: true },
|
||||
})
|
||||
: [],
|
||||
operatorIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [],
|
||||
]);
|
||||
|
||||
const parentIds = [
|
||||
...new Set(players.map((p) => p.parentId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
const parentAgents = parentIds.length
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const playerById = new Map(players.map((p) => [p.id.toString(), p]));
|
||||
const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));
|
||||
const parentById = new Map(parentAgents.map((a) => [a.id.toString(), a.username]));
|
||||
|
||||
return {
|
||||
items: rows.map((row) => {
|
||||
const player = playerById.get(row.userId.toString());
|
||||
const parentId = player?.parentId;
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
transactionId: row.transactionId,
|
||||
playerId: row.userId.toString(),
|
||||
playerUsername: player?.username ?? null,
|
||||
parentAgentId: parentId?.toString() ?? null,
|
||||
parentAgentUsername: parentId ? (parentById.get(parentId.toString()) ?? null) : null,
|
||||
transactionType: row.transactionType,
|
||||
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,
|
||||
betNo: row.referenceType === 'BET' ? row.referenceId : null,
|
||||
operatorId: row.operatorId?.toString() ?? null,
|
||||
operatorUsername: row.operatorId
|
||||
? (operatorById.get(row.operatorId.toString()) ?? null)
|
||||
: null,
|
||||
remark: row.remark,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async listTransferTransactions(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
Reference in New Issue
Block a user