artisan('lottery:admin-auth-sync')->assertExitCode(0); ensureRootAgentProfileSeeded(); }); test('super admin can update agent profile with capability flags', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'profile_super', 'name' => 'Profile Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'profile-child', 'name' => 'Profile Child', 'username' => 'profile_child', 'total_share_rate' => 10, 'credit_limit' => 1000, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'total_share_rate' => 12, 'credit_limit' => 1200, 'rebate_limit' => 1, 'default_player_rebate' => 0.5, 'can_grant_extra_rebate' => false, 'can_create_child_agent' => true, 'can_create_player' => true, ]) ->assertOk() ->assertJsonPath('data.total_share_rate', 12) ->assertJsonPath('data.can_create_child_agent', true); }); test('super admin can update agent login username and tree reflects it', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'username_super', 'name' => 'Username Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'username-child', 'name' => 'Username Child', 'username' => 'old_login', ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id, [ 'username' => 'new_login', ]) ->assertOk() ->assertJsonPath('data.username', 'new_login'); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/agent-nodes/tree?admin_site_id='.$siteId) ->assertOk() ->assertJsonPath('data.tree.0.children.0.username', 'new_login'); }); test('update can provision primary login when agent node had no bound account', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'provision_super', 'name' => 'Provision Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'provision-child', 'name' => 'Provision Child', 'username' => 'old_bound_login', ])); $userId = (int) DB::table('admin_user_agents')->where('agent_node_id', $child->id)->value('admin_user_id'); DB::table('admin_user_agent_roles')->where('agent_node_id', $child->id)->delete(); DB::table('admin_user_agents')->where('agent_node_id', $child->id)->delete(); AdminUser::query()->where('id', $userId)->delete(); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id, [ 'username' => 'fresh_login', 'password' => agentNodeTestPassword(), ]) ->assertOk() ->assertJsonPath('data.username', 'fresh_login'); expect( DB::table('admin_user_agents')->where('agent_node_id', $child->id)->where('is_primary', true)->count() )->toBe(1); }); test('agent profile update normalizes empty settlement cycle', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'profile_super3', 'name' => 'Profile Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'profile-child3', 'name' => 'Profile Child 3', 'username' => 'profile_child3', ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'settlement_cycle' => '', ]) ->assertOk() ->assertJsonPath('data.settlement_cycle', 'weekly'); }); test('bound agent cannot update own profile share and credit', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'self_profile_super', 'name' => 'Self Profile Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $agentNode = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'self-profile-agent', 'name' => 'Self Profile Agent', 'username' => 'self_profile_agent', 'total_share_rate' => 20, 'credit_limit' => 4000, ])); $agentUser = AdminUser::query()->where('username', 'self_profile_agent')->firstOrFail(); bindAdminUserToAgent($agentUser, $agentNode->id); $agentUser->syncPrimaryPlatformAgentRole($agentNode->id); $token = $agentUser->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$agentNode->id.'/profile', [ 'total_share_rate' => 99, 'credit_limit' => 999_999, 'can_create_player' => false, ]) ->assertForbidden(); }); test('site admin cannot change root agent credit limit via profile update', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $roleId = \App\Support\SitePlatformRole::id(); $siteAdmin = AdminUser::query()->create([ 'username' => 'root_credit_site_admin', 'name' => 'Root Credit Site Admin', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $siteAdmin->id, 'site_id' => $siteId, 'role_id' => $roleId, 'granted_at' => now(), ]); $token = $siteAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$rootId.'/profile', [ 'credit_limit' => 9_999_999, 'rebate_limit' => 0.5, ]) ->assertForbidden(); }); test('super admin can change root agent credit limit', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $super = AdminUser::query()->create([ 'username' => 'root_credit_super', 'name' => 'Root Credit Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$rootId.'/profile', [ 'credit_limit' => 88_888, ]) ->assertOk() ->assertJsonPath('data.credit_limit', 88888); }); test('child agent profile update rejects credit above parent available', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); \App\Models\AgentProfile::query()->updateOrCreate( ['agent_node_id' => $rootId], [ 'total_share_rate' => 100, 'credit_limit' => 5000, 'allocated_credit' => 0, 'used_credit' => 0, 'rebate_limit' => 1, 'default_player_rebate' => 0, ], ); $super = AdminUser::query()->create([ 'username' => 'child_credit_super', 'name' => 'Child Credit Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'child-credit-cap', 'name' => 'Child Credit Cap', 'username' => 'child_credit_cap', 'credit_limit' => 1000, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'credit_limit' => 9000, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('child agent profile update rejects rebate above parent', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); \App\Models\AgentProfile::query()->updateOrCreate( ['agent_node_id' => $rootId], [ 'total_share_rate' => 100, 'credit_limit' => 10000, 'allocated_credit' => 0, 'used_credit' => 0, 'rebate_limit' => 0.2, 'default_player_rebate' => 0, ], ); $super = AdminUser::query()->create([ 'username' => 'child_rebate_super', 'name' => 'Child Rebate Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'child-rebate-cap', 'name' => 'Child Rebate Cap', 'username' => 'child_rebate_cap', 'rebate_limit' => 1, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'rebate_limit' => 50, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('agent profile update rejects default rebate above limit', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); \App\Models\AgentProfile::query()->updateOrCreate( ['agent_node_id' => $rootId], [ 'total_share_rate' => 100, 'credit_limit' => 10000, 'allocated_credit' => 0, 'used_credit' => 0, 'rebate_limit' => 1, 'default_player_rebate' => 0, ], ); $super = AdminUser::query()->create([ 'username' => 'profile_super2', 'name' => 'Profile Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'profile-child2', 'name' => 'Profile Child 2', 'username' => 'profile_child2', ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'rebate_limit' => 0.5, 'default_player_rebate' => 1, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('partial agent profile update preserves unchanged share rate', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'partial_profile_super', 'name' => 'Partial Profile Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'partial-profile-child', 'name' => 'Partial Profile Child', 'username' => 'partial_profile_child', 'total_share_rate' => 15, 'credit_limit' => 2000, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'credit_limit' => 2500, ]) ->assertOk() ->assertJsonPath('data.total_share_rate', 15) ->assertJsonPath('data.credit_limit', 2500); }); test('agent node update rejects chinese username', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'cn_username_super', 'name' => 'CN Username Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'cn-username-child', 'name' => 'CN Username Child', 'username' => 'cn_child_login', ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id, [ 'username' => '中文账号', ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); });