insertGetId([ 'code' => $code, 'name' => $code, 'is_default' => false, 'created_at' => now(), 'updated_at' => now(), ]); $rootId = (int) DB::table('agent_nodes')->insertGetId([ 'admin_site_id' => $siteId, 'parent_id' => null, 'depth' => 0, 'path' => '/', 'code' => $code, 'name' => 'Root', 'status' => 1, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('agent_nodes')->where('id', $rootId)->update(['path' => "/{$rootId}/"]); AgentProfile::query()->create([ 'agent_node_id' => $rootId, 'total_share_rate' => 60, 'credit_limit' => $rootCreditLimit, 'allocated_credit' => 0, 'used_credit' => 0, 'rebate_limit' => 0.01, 'default_player_rebate' => 0.005, ]); $childId = (int) DB::table('agent_nodes')->insertGetId([ 'admin_site_id' => $siteId, 'parent_id' => $rootId, 'depth' => 1, 'path' => "/{$rootId}/", 'code' => $code.'-child', 'name' => 'Child', 'status' => 1, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('agent_nodes')->where('id', $childId)->update(['path' => "/{$rootId}/{$childId}/"]); AgentProfile::query()->create([ 'agent_node_id' => $childId, 'total_share_rate' => 30, 'credit_limit' => 5000, 'allocated_credit' => 0, 'used_credit' => 0, 'rebate_limit' => 0.008, 'default_player_rebate' => 0.003, ]); return [ 'site_id' => $siteId, 'root' => AgentNode::query()->findOrFail($rootId), 'child' => AgentNode::query()->findOrFail($childId), ]; } function createCreditPlayerForUsedCredit(string $siteCode, int $agentNodeId, string $playerSuffix): int { $playerId = (int) DB::table('players')->insertGetId([ 'site_code' => $siteCode, 'agent_node_id' => $agentNodeId, 'site_player_id' => 'uc-'.$playerSuffix, 'auth_source' => 'lottery_native', 'funding_mode' => 'credit', 'username' => 'uc-'.$playerSuffix, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $playerId, 'credit_limit' => 3000, 'used_credit' => 0, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); return $playerId; } test('bet hold syncs used_credit up the agent chain', function (): void { $line = createAgentLineForUsedCredit('uc-hold', 10000); $playerId = createCreditPlayerForUsedCredit('uc-hold', $line['child']->id, 'p1'); $player = \App\Models\Player::query()->find($playerId); app(PlayerCreditService::class)->assertMayPlaceBet($player, 500); $childProfile = AgentProfile::query()->where('agent_node_id', $line['child']->id)->first(); $rootProfile = AgentProfile::query()->where('agent_node_id', $line['root']->id)->first(); expect((int) $childProfile->used_credit)->toBe(5); expect((int) $rootProfile->used_credit)->toBe(5); }); test('settled win decreases used_credit up the agent chain', function (): void { $line = createAgentLineForUsedCredit('uc-win', 10000); $playerId = createCreditPlayerForUsedCredit('uc-win', $line['child']->id, 'p2'); $player = \App\Models\Player::query()->find($playerId); $credit = app(PlayerCreditService::class); $credit->assertMayPlaceBet($player, 500); $credit->applySettledWin($player, 200, 9991); $childProfile = AgentProfile::query()->where('agent_node_id', $line['child']->id)->first(); $rootProfile = AgentProfile::query()->where('agent_node_id', $line['root']->id)->first(); expect((int) $childProfile->used_credit)->toBe(3); expect((int) $rootProfile->used_credit)->toBe(3); }); test('used_credit does not participate in available_credit calculation', function (): void { $line = createAgentLineForUsedCredit('uc-avail', 10000); $playerId = createCreditPlayerForUsedCredit('uc-avail', $line['child']->id, 'p3'); $player = \App\Models\Player::query()->find($playerId); app(PlayerCreditService::class)->assertMayPlaceBet($player, 500); $rootProfile = AgentProfile::query()->where('agent_node_id', $line['root']->id)->first(); expect((int) $rootProfile->used_credit)->toBe(5); $available = max(0, (int) $rootProfile->credit_limit - (int) $rootProfile->allocated_credit); expect($available)->toBe(10000); }); test('syncForAgentId recalculates from subtree players', function (): void { $line = createAgentLineForUsedCredit('uc-sync', 10000); $playerId = createCreditPlayerForUsedCredit('uc-sync', $line['child']->id, 'p4'); DB::table('player_credit_accounts')->where('player_id', $playerId)->update([ 'used_credit' => 100, 'frozen_credit' => 50, ]); $sync = app(AgentUsedCreditSyncService::class); $sync->syncForAgentId($line['root']->id); $rootProfile = AgentProfile::query()->where('agent_node_id', $line['root']->id)->first(); expect((int) $rootProfile->used_credit)->toBe(150); $sync->syncForAgentId($line['child']->id); $childProfile = AgentProfile::query()->where('agent_node_id', $line['child']->id)->first(); expect((int) $childProfile->used_credit)->toBe(150); }); test('wallet players do not contribute to used_credit', function (): void { $line = createAgentLineForUsedCredit('uc-wallet', 10000); $walletPlayerId = (int) DB::table('players')->insertGetId([ 'site_code' => 'uc-wallet', 'agent_node_id' => $line['child']->id, 'site_player_id' => 'uc-wp', 'auth_source' => 'main_site_sso', 'funding_mode' => 'wallet', 'username' => 'uc-wp', 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $walletPlayerId, 'credit_limit' => 5000, 'used_credit' => 200, 'frozen_credit' => 100, 'created_at' => now(), 'updated_at' => now(), ]); $sync = app(AgentUsedCreditSyncService::class); $sync->syncForAgentId($line['root']->id); $rootProfile = AgentProfile::query()->where('agent_node_id', $line['root']->id)->first(); expect((int) $rootProfile->used_credit)->toBe(0); });