import { Decimal } from '@prisma/client/runtime/library'; import { AgentsService } from './agents.service'; import { createPrismaMock } from '../../testing/prisma-mock'; describe('AgentsService', () => { const parentAgentId = 1n; const createdAgentId = 2n; const tx = { user: { create: jest.fn(), findUnique: jest.fn(), update: jest.fn(), }, userAuth: { create: jest.fn(), }, userPreference: { create: jest.fn(), }, userInvite: { create: jest.fn(), findUnique: jest.fn(), }, agentProfile: { create: jest.fn(), findUnique: jest.fn(), }, agentClosure: { create: jest.fn(), findMany: jest.fn(), }, }; const prismaBase = { ...tx, agentProfile: { ...tx.agentProfile, update: jest.fn(), }, user: { ...tx.user, updateMany: jest.fn(), }, }; const prisma = createPrismaMock(prismaBase) as typeof prismaBase & { $transaction: jest.Mock }; prisma.$transaction.mockImplementation(async (arg: unknown) => { if (Array.isArray(arg)) { return Promise.all(arg); } return (arg as (client: typeof tx) => Promise)(tx); }); const auth = { hashPassword: jest.fn(), }; const systemConfig = { getAgentHierarchySettings: jest.fn(), getAgentSuspendSettings: jest.fn(), }; const network = {}; const credit = { recalculateUsedCredit: jest.fn(), }; let service: AgentsService; beforeEach(() => { jest.clearAllMocks(); service = new AgentsService( prisma as never, auth as never, systemConfig as never, network as never, credit as never, ); systemConfig.getAgentHierarchySettings.mockResolvedValue({ maxAgentLevel: 3, defaultSubAgentCreditRatio: 50, }); systemConfig.getAgentSuspendSettings.mockResolvedValue({ suspendFreezeDirectPlayers: true, suspendBlockPlayerLogin: true, }); auth.hashPassword.mockResolvedValue('hashed-password'); prisma.agentProfile.findUnique.mockResolvedValue({ userId: parentAgentId, level: 1, creditLimit: new Decimal(10000), usedCredit: new Decimal(2000), cashbackRate: new Decimal(10), maxSingleDeposit: null, maxDailyDeposit: null, }); tx.user.create.mockResolvedValue({ id: createdAgentId, username: 'agent-child', userType: 'AGENT', }); tx.user.findUnique.mockResolvedValue(null); tx.user.update.mockResolvedValue({}); tx.userInvite.findUnique.mockResolvedValue(null); tx.userInvite.create.mockResolvedValue({}); tx.userAuth.create.mockResolvedValue({}); tx.userPreference.create.mockResolvedValue({}); tx.agentProfile.create.mockResolvedValue({}); tx.agentClosure.create.mockResolvedValue({}); tx.agentClosure.findMany.mockResolvedValue([ { ancestorId: parentAgentId, depth: 0 }, ]); }); it('recalculates parent credit exposure after creating a child agent', async () => { await service.createAgent(99n, { username: 'agent-child', password: 'secret', level: 2, parentAgentId, }); expect(tx.agentProfile.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ creditLimit: 4000 }), }), ); expect(credit.recalculateUsedCredit).toHaveBeenCalledWith(parentAgentId); expect(credit.recalculateUsedCredit).not.toHaveBeenCalledWith(createdAgentId); }); it('applies global suspend defaults when suspending without explicit flags', async () => { jest.spyOn(service, 'getAgentAdminDetail').mockResolvedValue({ userId: '10' } as never); prisma.agentProfile.findUnique.mockResolvedValue({ userId: 10n, user: { username: 'agent-a', locale: 'zh-CN' }, parentAgentId: null, }); prisma.user.updateMany.mockResolvedValue({ count: 2 }); prisma.agentProfile.update.mockResolvedValue({}); prisma.user.update.mockResolvedValue({}); await service.updateAgentAdmin(10n, { status: 'SUSPENDED' }); expect(systemConfig.getAgentSuspendSettings).toHaveBeenCalled(); expect(prisma.user.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ parentId: 10n, userType: 'PLAYER' }), data: { status: 'SUSPENDED' }, }), ); expect(prisma.agentProfile.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ blockDirectPlayerLogin: true }), }), ); }); });