authorizeManage($actor, $workspace); $result = $this->persistWorkspaceSetting($workspace, $domain, $key, $value, (int) $actor->getKey()); $this->resolver->clearCache(); $afterValue = $this->resolver->resolveValue($workspace, $domain, $key); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceSettingUpdated->value, context: [ 'metadata' => [ 'scope' => 'workspace', 'domain' => $domain, 'key' => $key, 'before_value' => $result['before_value'], 'after_value' => $afterValue, ], ], actor: $actor, resourceType: 'workspace_setting', resourceId: $domain.'.'.$key, ); return $result['setting']; } public function updateWorkspaceCommercialLifecycle( PlatformUser $actor, Workspace $workspace, string $state, string $reason, ): void { $state = strtolower(trim($state)); $reason = trim($reason); if (! $actor->hasCapability(PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE)) { throw new AuthorizationException('Missing commercial lifecycle manage capability.'); } if ($reason === '') { throw ValidationException::withMessages([ 'reason' => ['A rationale is required when changing commercial lifecycle state.'], ]); } DB::transaction(function () use ($actor, $workspace, $state, $reason): void { $stateResult = $this->persistWorkspaceSetting( workspace: $workspace, domain: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN, key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE, value: $state, updatedByUserId: null, ); $reasonResult = $this->persistWorkspaceSetting( workspace: $workspace, domain: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN, key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON, value: $reason, updatedByUserId: null, ); $this->resolver->clearCache(); $afterState = $this->resolver->resolveValue( $workspace, WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN, WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE, ); $afterReason = $this->resolver->resolveValue( $workspace, WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN, WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON, ); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceSettingUpdated->value, context: [ 'metadata' => [ 'scope' => 'workspace', 'domain' => WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN, 'key' => WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE, 'before_state' => $stateResult['before_value'], 'after_state' => $afterState, 'before_reason' => $reasonResult['before_value'], 'after_reason' => $afterReason, ], ], actor: $actor, resourceType: 'workspace_setting', resourceId: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN.'.'.WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE, targetLabel: 'Commercial lifecycle state', ); }); } public function resetWorkspaceSetting(User $actor, Workspace $workspace, string $domain, string $key): void { $this->authorizeManage($actor, $workspace); $this->requireDefinition($domain, $key); $existing = WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', $domain) ->where('key', $key) ->first(); $beforeValue = $existing instanceof WorkspaceSetting ? $this->decodeStoredValue($existing->getAttribute('value')) : null; if ($existing instanceof WorkspaceSetting) { $existing->delete(); } $this->resolver->clearCache(); $afterValue = $this->resolver->resolveValue($workspace, $domain, $key); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceSettingReset->value, context: [ 'metadata' => [ 'scope' => 'workspace', 'domain' => $domain, 'key' => $key, 'before_value' => $beforeValue, 'after_value' => $afterValue, ], ], actor: $actor, resourceType: 'workspace_setting', resourceId: $domain.'.'.$key, ); } public function updateTenantSetting(User $actor, Workspace $workspace, Tenant $tenant, string $domain, string $key, mixed $value): TenantSetting { $this->authorizeManage($actor, $workspace); $this->assertTenantBelongsToWorkspace($workspace, $tenant); $definition = $this->requireDefinition($domain, $key); $normalizedValue = $this->validatedValue($definition, $value); $setting = TenantSetting::query()->updateOrCreate([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'domain' => $domain, 'key' => $key, ], [ 'value' => $normalizedValue, 'updated_by_user_id' => (int) $actor->getKey(), ]); $this->resolver->clearCache(); return $setting; } public function resetTenantSetting(User $actor, Workspace $workspace, Tenant $tenant, string $domain, string $key): void { $this->authorizeManage($actor, $workspace); $this->assertTenantBelongsToWorkspace($workspace, $tenant); $this->requireDefinition($domain, $key); TenantSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('tenant_id', (int) $tenant->getKey()) ->where('domain', $domain) ->where('key', $key) ->delete(); $this->resolver->clearCache(); } private function requireDefinition(string $domain, string $key): SettingDefinition { $definition = $this->registry->find($domain, $key); if ($definition instanceof SettingDefinition) { return $definition; } throw ValidationException::withMessages([ 'key' => [sprintf('Unknown setting key: %s.%s', $domain, $key)], ]); } /** * @return array{setting: WorkspaceSetting, before_value: mixed} */ private function persistWorkspaceSetting(Workspace $workspace, string $domain, string $key, mixed $value, ?int $updatedByUserId): array { $definition = $this->requireDefinition($domain, $key); $normalizedValue = $this->validatedValue($definition, $value); $existing = WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', $domain) ->where('key', $key) ->first(); $beforeValue = $existing instanceof WorkspaceSetting ? $this->decodeStoredValue($existing->getAttribute('value')) : null; $setting = WorkspaceSetting::query()->updateOrCreate([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => $domain, 'key' => $key, ], [ 'value' => $normalizedValue, 'updated_by_user_id' => $updatedByUserId, ]); return [ 'setting' => $setting, 'before_value' => $beforeValue, ]; } private function validatedValue(SettingDefinition $definition, mixed $value): mixed { $validator = Validator::make( data: ['value' => $value], rules: ['value' => $definition->rules], ); if ($validator->fails()) { throw ValidationException::withMessages($validator->errors()->toArray()); } return $definition->normalize($validator->validated()['value']); } private function authorizeManage(User $actor, Workspace $workspace): void { if (! $this->workspaceCapabilityResolver->isMember($actor, $workspace)) { throw new NotFoundHttpException('Workspace not found.'); } if (! $this->workspaceCapabilityResolver->can($actor, $workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) { throw new AuthorizationException('Missing workspace settings manage capability.'); } } private function assertTenantBelongsToWorkspace(Workspace $workspace, Tenant $tenant): void { if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) { throw new NotFoundHttpException('Tenant is outside the selected workspace scope.'); } } private function decodeStoredValue(mixed $value): mixed { if (! is_string($value)) { return $value; } $decoded = json_decode($value, true); return json_last_error() === JSON_ERROR_NONE ? $decoded : $value; } }