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:
@@ -25,14 +25,61 @@ export class UsersService {
|
||||
parent?: {
|
||||
username: string;
|
||||
agentLevel: number | null;
|
||||
parent?: { username: string; agentLevel: number | null } | null;
|
||||
parent?: { username: string; agentLevel: number | null; parent?: unknown } | null;
|
||||
} | null,
|
||||
chain?: string[],
|
||||
): string[] {
|
||||
if (chain?.length) return chain;
|
||||
if (!parent) return [];
|
||||
if (parent.agentLevel === 2 && parent.parent?.username) {
|
||||
return [parent.parent.username, parent.username];
|
||||
const ancestors: string[] = [];
|
||||
let cur: typeof parent | null | undefined = parent;
|
||||
while (cur) {
|
||||
ancestors.unshift(cur.username);
|
||||
cur = cur.parent as typeof parent | null | undefined;
|
||||
}
|
||||
return [parent.username];
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
private async buildAffiliationChainMap(parentIds: (bigint | null | undefined)[]) {
|
||||
const map = new Map<string, string[]>();
|
||||
const pending = new Set<bigint>();
|
||||
const cache = new Map<string, { username: string; parentId: bigint | null }>();
|
||||
|
||||
for (const pid of parentIds) {
|
||||
if (pid) pending.add(pid);
|
||||
}
|
||||
|
||||
while (pending.size > 0) {
|
||||
const batch = [...pending];
|
||||
pending.clear();
|
||||
const agents = await this.prisma.user.findMany({
|
||||
where: { id: { in: batch }, userType: 'AGENT', deletedAt: null },
|
||||
select: { id: true, username: true, parentId: true },
|
||||
});
|
||||
for (const agent of agents) {
|
||||
cache.set(agent.id.toString(), { username: agent.username, parentId: agent.parentId });
|
||||
if (agent.parentId && !cache.has(agent.parentId.toString())) {
|
||||
pending.add(agent.parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const build = (startId: bigint | null | undefined): string[] => {
|
||||
const chain: string[] = [];
|
||||
let cur = startId ?? null;
|
||||
while (cur) {
|
||||
const hit = cache.get(cur.toString());
|
||||
if (!hit) break;
|
||||
chain.unshift(hit.username);
|
||||
cur = hit.parentId;
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
|
||||
for (const pid of parentIds) {
|
||||
if (pid) map.set(pid.toString(), build(pid));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private formatPlayerRow(
|
||||
@@ -58,8 +105,9 @@ export class UsersService {
|
||||
auth?: { lastLoginAt: Date | null } | null;
|
||||
},
|
||||
bet?: { count: number; totalStake: string; totalReturn: string },
|
||||
affiliationChain?: string[],
|
||||
) {
|
||||
const affiliationAgents = this.buildAffiliationAgents(u.parent);
|
||||
const affiliationAgents = this.buildAffiliationAgents(u.parent, affiliationChain);
|
||||
return {
|
||||
id: u.id.toString(),
|
||||
username: u.username,
|
||||
@@ -242,8 +290,15 @@ export class UsersService {
|
||||
]);
|
||||
|
||||
const betMap = await this.loadBetStatsMap(rows.map((r) => r.id));
|
||||
const affiliationMap = await this.buildAffiliationChainMap(rows.map((r) => r.parentId));
|
||||
return {
|
||||
items: rows.map((u) => this.formatPlayerRow(u, betMap.get(u.id.toString()))),
|
||||
items: rows.map((u) =>
|
||||
this.formatPlayerRow(
|
||||
u,
|
||||
betMap.get(u.id.toString()),
|
||||
u.parentId ? affiliationMap.get(u.parentId.toString()) : undefined,
|
||||
),
|
||||
),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -269,6 +324,7 @@ export class UsersService {
|
||||
});
|
||||
if (!user) throw appNotFound('PLAYER_NOT_FOUND');
|
||||
|
||||
const affiliationMap = await this.buildAffiliationChainMap([user.parentId]);
|
||||
const [betCount, betStake] = await Promise.all([
|
||||
this.prisma.bet.count({ where: { userId: playerId } }),
|
||||
this.prisma.bet.aggregate({
|
||||
@@ -278,7 +334,11 @@ export class UsersService {
|
||||
]);
|
||||
|
||||
return {
|
||||
...this.formatPlayerRow(user),
|
||||
...this.formatPlayerRow(
|
||||
user,
|
||||
undefined,
|
||||
user.parentId ? affiliationMap.get(user.parentId.toString()) : undefined,
|
||||
),
|
||||
lastLoginAt: user.auth?.lastLoginAt ?? null,
|
||||
loginFailCount: user.auth?.loginFailCount ?? 0,
|
||||
lockedUntil: user.auth?.lockedUntil ?? null,
|
||||
|
||||
Reference in New Issue
Block a user