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('agent profile update rejects credit below already allocated amount', 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' => 'below_allocated_super', 'name' => 'Below Allocated Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $agent = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'below-allocated-agent', 'name' => 'Below Allocated Agent', 'username' => 'below_allocated_agent', 'credit_limit' => 5000, ])); $service->createChild($super, agentChildPayload([ 'parent_id' => $agent->id, 'code' => 'below-allocated-child', 'name' => 'Below Allocated Child', 'username' => 'below_allocated_child', 'credit_limit' => 3000, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$agent->id.'/profile', [ 'credit_limit' => 2000, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); $profile = DB::table('agent_profiles')->where('agent_node_id', $agent->id)->first(); expect((int) $profile->credit_limit)->toBe(5000) ->and((int) $profile->allocated_credit)->toBe(3000); }); 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('lowering agent rebate limit clamps descendant agent and player rebate profiles', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code'); $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, 'can_grant_extra_rebate' => true, ], ); $super = AdminUser::query()->create([ 'username' => 'clamp_rebate_super', 'name' => 'Clamp Rebate Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $agent = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'clamp-rebate-agent', 'name' => 'Clamp Rebate Agent', 'username' => 'clamp_rebate_agent', 'rebate_limit' => 10, 'default_player_rebate' => 6, 'can_grant_extra_rebate' => true, ])); $childAgent = $service->createChild($super, agentChildPayload([ 'parent_id' => $agent->id, 'code' => 'clamp-rebate-child', 'name' => 'Clamp Rebate Child', 'username' => 'clamp_rebate_child', 'rebate_limit' => 8, 'default_player_rebate' => 6, 'can_grant_extra_rebate' => true, ])); $grandchildAgent = $service->createChild($super, agentChildPayload([ 'parent_id' => $childAgent->id, 'code' => 'clamp-rebate-grandchild', 'name' => 'Clamp Rebate Grandchild', 'username' => 'clamp_rebate_grandchild', 'rebate_limit' => 7, 'default_player_rebate' => 5, 'can_grant_extra_rebate' => true, ])); $directPlayer = \App\Models\Player::query()->create([ 'site_code' => $siteCode, 'agent_node_id' => $agent->id, 'site_player_id' => 'clamp-direct-player', 'username' => 'clamp_direct_player', 'default_currency' => 'NPR', 'status' => 0, ]); $downlinePlayer = \App\Models\Player::query()->create([ 'site_code' => $siteCode, 'agent_node_id' => $childAgent->id, 'site_player_id' => 'clamp-downline-player', 'username' => 'clamp_downline_player', 'default_currency' => 'NPR', 'status' => 0, ]); $grandchildPlayer = \App\Models\Player::query()->create([ 'site_code' => $siteCode, 'agent_node_id' => $grandchildAgent->id, 'site_player_id' => 'clamp-grandchild-player', 'username' => 'clamp_grandchild_player', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_rebate_profiles')->insert([ [ 'player_id' => $directPlayer->id, 'game_type' => '*', 'inherit_from_agent' => false, 'rebate_rate' => 0.10, 'extra_rebate_rate' => 0.03, 'created_at' => now(), 'updated_at' => now(), ], [ 'player_id' => $downlinePlayer->id, 'game_type' => '*', 'inherit_from_agent' => false, 'rebate_rate' => 0.10, 'extra_rebate_rate' => 0.03, 'created_at' => now(), 'updated_at' => now(), ], [ 'player_id' => $grandchildPlayer->id, 'game_type' => '*', 'inherit_from_agent' => false, 'rebate_rate' => 0.07, 'extra_rebate_rate' => 0.01, 'created_at' => now(), 'updated_at' => now(), ], ]); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$agent->id.'/profile', [ 'rebate_limit' => 2, 'default_player_rebate' => 0, 'can_grant_extra_rebate' => false, ]) ->assertOk() ->assertJsonPath('data.rebate_limit', 2); $directProfile = DB::table('player_rebate_profiles')->where('player_id', $directPlayer->id)->first(); expect(round((float) $directProfile->rebate_rate, 4))->toBe(0.02) ->and(round((float) $directProfile->extra_rebate_rate, 4))->toBe(0.0); $childProfile = DB::table('agent_profiles')->where('agent_node_id', $childAgent->id)->first(); expect(round((float) $childProfile->rebate_limit, 4))->toBe(0.02) ->and(round((float) $childProfile->default_player_rebate, 4))->toBe(0.02) ->and((bool) $childProfile->can_grant_extra_rebate)->toBeFalse(); $downlineProfile = DB::table('player_rebate_profiles')->where('player_id', $downlinePlayer->id)->first(); expect(round((float) $downlineProfile->rebate_rate, 4))->toBe(0.02) ->and(round((float) $downlineProfile->extra_rebate_rate, 4))->toBe(0.0); $grandchildProfile = DB::table('agent_profiles')->where('agent_node_id', $grandchildAgent->id)->first(); expect(round((float) $grandchildProfile->rebate_limit, 4))->toBe(0.02) ->and(round((float) $grandchildProfile->default_player_rebate, 4))->toBe(0.02) ->and((bool) $grandchildProfile->can_grant_extra_rebate)->toBeFalse(); $grandchildPlayerProfile = DB::table('player_rebate_profiles')->where('player_id', $grandchildPlayer->id)->first(); expect(round((float) $grandchildPlayerProfile->rebate_rate, 4))->toBe(0.02) ->and(round((float) $grandchildPlayerProfile->extra_rebate_rate, 4))->toBe(0.0); }); test('agent profile update rejects enabling extra rebate when parent disallows 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); \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, 'can_grant_extra_rebate' => true, ], ); $super = AdminUser::query()->create([ 'username' => 'extra_rebate_super', 'name' => 'Extra Rebate Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $parent = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'extra-rebate-parent', 'name' => 'Extra Rebate Parent', 'username' => 'extra_rebate_parent', 'rebate_limit' => 10, 'can_grant_extra_rebate' => false, ])); $child = $service->createChild($super, agentChildPayload([ 'parent_id' => $parent->id, 'code' => 'extra-rebate-child', 'name' => 'Extra Rebate Child', 'username' => 'extra_rebate_child', 'rebate_limit' => 2, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'can_grant_extra_rebate' => true, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('lowering agent share rate clamps descendant agent share rates', 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' => 'clamp_share_super', 'name' => 'Clamp Share Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $agent = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'clamp-share-agent', 'name' => 'Clamp Share Agent', 'username' => 'clamp_share_agent', 'total_share_rate' => 60, ])); $childAgent = $service->createChild($super, agentChildPayload([ 'parent_id' => $agent->id, 'code' => 'clamp-share-child', 'name' => 'Clamp Share Child', 'username' => 'clamp_share_child', 'total_share_rate' => 40, ])); $grandchildAgent = $service->createChild($super, agentChildPayload([ 'parent_id' => $childAgent->id, 'code' => 'clamp-share-grandchild', 'name' => 'Clamp Share Grandchild', 'username' => 'clamp_share_grandchild', 'total_share_rate' => 25, ])); $lowShareGrandchild = $service->createChild($super, agentChildPayload([ 'parent_id' => $childAgent->id, 'code' => 'clamp-share-low', 'name' => 'Clamp Share Low', 'username' => 'clamp_share_low', 'total_share_rate' => 15, ])); $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$agent->id.'/profile', [ 'total_share_rate' => 20, ]) ->assertOk() ->assertJsonPath('data.total_share_rate', 20); $childProfile = DB::table('agent_profiles')->where('agent_node_id', $childAgent->id)->first(); expect(round((float) $childProfile->total_share_rate, 4))->toBe(20.0); $grandchildProfile = DB::table('agent_profiles')->where('agent_node_id', $grandchildAgent->id)->first(); expect(round((float) $grandchildProfile->total_share_rate, 4))->toBe(20.0); $lowShareGrandchildProfile = DB::table('agent_profiles')->where('agent_node_id', $lowShareGrandchild->id)->first(); expect(round((float) $lowShareGrandchildProfile->total_share_rate, 4))->toBe(15.0); }); 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); });