$payload */ public function upsertForNode(AgentNode $node, array $payload, ?AgentNode $parent = null): AgentProfile { $parent = $parent ?? ($node->parent_id !== null ? AgentNode::query()->find($node->parent_id) : null); $totalShare = (float) ($payload['total_share_rate'] ?? 0); $creditLimit = (int) ($payload['credit_limit'] ?? 0); $rebateLimit = (float) ($payload['rebate_limit'] ?? 0); $defaultRebate = (float) ($payload['default_player_rebate'] ?? 0); if ($parent !== null) { $this->shareRateValidator->assertChildWithinParent($parent, $totalShare); } return DB::transaction(function () use ($node, $payload, $parent, $totalShare, $creditLimit, $rebateLimit, $defaultRebate): AgentProfile { $profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $node->id]); $previousCredit = (int) $profile->credit_limit; $isNew = ! $profile->exists; if (! $isNew && $creditLimit < (int) $profile->allocated_credit) { throw ValidationException::withMessages([ 'credit_limit' => ['below_allocated'], ]); } if ($parent !== null) { $delta = $isNew ? $creditLimit : max(0, $creditLimit - $previousCredit); if ($delta > 0) { $this->creditAllocationValidator->assertAllocationWithinParent($parent, $delta); } } if ($defaultRebate > $rebateLimit && $rebateLimit > 0) { throw \Illuminate\Validation\ValidationException::withMessages([ 'default_player_rebate' => ['exceeds_limit'], ]); } $profile->fill([ 'total_share_rate' => $totalShare, 'credit_limit' => $creditLimit, 'rebate_limit' => $rebateLimit, 'default_player_rebate' => $defaultRebate, 'settlement_cycle' => AgentSettlementCycle::normalize( $payload['settlement_cycle'] ?? $profile->settlement_cycle ?? 'weekly', ), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? $profile->can_grant_extra_rebate ?? false), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? ($isNew ? false : $profile->can_create_child_agent)), 'can_create_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)), ]); if (! $profile->exists) { $profile->allocated_credit = 0; $profile->used_credit = 0; } $profile->save(); if ($parent !== null) { $parentProfile = AgentProfile::query()->where('agent_node_id', $parent->id)->first(); if ($parentProfile !== null) { $creditDelta = $isNew ? $creditLimit : ($creditLimit - $previousCredit); if ($creditDelta !== 0) { $parentProfile->allocated_credit = max(0, (int) $parentProfile->allocated_credit + $creditDelta); $parentProfile->save(); } } } return $profile; }); } /** * @return array */ public function present(AgentProfile $profile): array { $available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit); return [ 'agent_node_id' => (int) $profile->agent_node_id, 'total_share_rate' => (float) $profile->total_share_rate, 'credit_limit' => (int) $profile->credit_limit, 'allocated_credit' => (int) $profile->allocated_credit, 'used_credit' => (int) $profile->used_credit, 'available_credit' => $available, 'rebate_limit' => (float) $profile->rebate_limit, 'default_player_rebate' => (float) $profile->default_player_rebate, 'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle), 'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate, 'can_create_child_agent' => (bool) $profile->can_create_child_agent, 'can_create_player' => (bool) $profile->can_create_player, ]; } public function profileForNode(int $agentNodeId): ?AgentProfile { return AgentProfile::query()->where('agent_node_id', $agentNodeId)->first(); } public function assertActorMayCreateChildAgent(AdminUser $admin): void { if ($admin->isSuperAdmin()) { return; } $node = AdminAgentScope::primaryAgentNode($admin); if ($node === null) { return; } if (! $this->nodeMayCreateChildAgent($node->id)) { throw ValidationException::withMessages([ 'parent_id' => ['cannot_create_child_agent'], ]); } } public function assertActorMayCreatePlayer(AdminUser $admin): void { if ($admin->isSuperAdmin()) { return; } $node = AdminAgentScope::primaryAgentNode($admin); if ($node === null) { return; } if (! $this->nodeMayCreatePlayer($node->id)) { throw ValidationException::withMessages([ 'site_code' => ['cannot_create_player'], ]); } } /** * @param array $childPayload */ public function assertChildCapabilityGrantsWithinParent(AgentNode $parent, array $childPayload, AdminUser $actor): void { if ($actor->isSuperAdmin()) { return; } $parentProfile = $this->profileForNode((int) $parent->id); if ((bool) ($childPayload['can_create_child_agent'] ?? false) && ! ($parentProfile?->can_create_child_agent ?? false)) { throw ValidationException::withMessages([ 'can_create_child_agent' => ['parent_cannot_delegate'], ]); } if ((bool) ($childPayload['can_create_player'] ?? true) && ! ($parentProfile?->can_create_player ?? false)) { throw ValidationException::withMessages([ 'can_create_player' => ['parent_cannot_delegate'], ]); } } public function nodeMayCreateChildAgent(int $agentNodeId): bool { $profile = $this->profileForNode($agentNodeId); return $profile === null || $profile->can_create_child_agent; } public function nodeMayCreatePlayer(int $agentNodeId): bool { $profile = $this->profileForNode($agentNodeId); return $profile === null || $profile->can_create_player; } }