Spaces:
Running
Running
feat(membership): admin-driven account creation, real estate tier/investment work, in-progress monorepo migration state
b2dcf0f | namespace App\Domain\Config; | |
| use App\Domain\Audit\AuditService; | |
| use App\Infrastructure\Ids\UuidGenerator; | |
| use App\Shared\Exceptions\ApiException; | |
| class ConfigService | |
| { | |
| public function __construct( | |
| private readonly ConfigRepository $settings, | |
| private readonly UuidGenerator $ids, | |
| private readonly AuditService $audit, | |
| ) { | |
| } | |
| public function getNamespace(string $namespace): array | |
| { | |
| return [ | |
| 'namespace' => $namespace, | |
| 'settings' => array_map(fn (array $row): array => $this->payload($row), $this->settings->listNamespace($namespace)), | |
| ]; | |
| } | |
| public function update(string $namespace, string $key, mixed $value, string $actorUserId): array | |
| { | |
| $this->validateValue($namespace, $key, $value); | |
| $id = $this->settings->createVersion($this->ids->generate(), $namespace, $key, $value); | |
| $this->audit->record('CONFIG_SETTING_UPDATED', $actorUserId, null, 'configuration_settings', $id, 'SUCCESS', [ | |
| 'namespace' => $namespace, | |
| 'key' => $key, | |
| ]); | |
| return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))]; | |
| } | |
| public function publish(string $namespace, string $key, string $actorUserId): array | |
| { | |
| $setting = $this->settings->latest($namespace, $key); | |
| if ($setting === null) { | |
| throw new ApiException('CONFIG_NOT_FOUND', 'Configuration setting was not found.', [], 404); | |
| } | |
| $this->settings->publish($setting->id, $actorUserId); | |
| $this->audit->record('CONFIG_SETTING_PUBLISHED', $actorUserId, null, 'configuration_settings', $setting->id, 'SUCCESS', [ | |
| 'namespace' => $namespace, | |
| 'key' => $key, | |
| ]); | |
| return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))]; | |
| } | |
| public function versions(string $namespace, string $key): array | |
| { | |
| return [ | |
| 'namespace' => $namespace, | |
| 'key' => $key, | |
| 'versions' => array_map(fn (array $row): array => $this->payload($row), $this->settings->versions($namespace, $key)), | |
| ]; | |
| } | |
| private function payload(array $row): array | |
| { | |
| return [ | |
| 'id' => $row['id'], | |
| 'namespace' => $row['namespace'], | |
| 'key' => $row['setting_key'], | |
| 'value' => json_decode($row['setting_value'], true, flags: JSON_THROW_ON_ERROR), | |
| 'status' => $row['status'], | |
| 'version_number' => (int) $row['version_number'], | |
| 'published_at' => $row['published_at'], | |
| 'updated_at' => $row['updated_at'], | |
| ]; | |
| } | |
| private function validateValue(string $namespace, string $key, mixed $value): void | |
| { | |
| if (str_contains(strtolower($key), 'secret') || str_contains(strtolower($key), 'password')) { | |
| throw new ApiException('SECRET_VALUE_NOT_ALLOWED', 'Raw secrets must not be stored in configuration settings.', [], 422); | |
| } | |
| if (str_starts_with($namespace.'.'.$key, 'fees.') && (! is_numeric($value) || (float) $value < 0)) { | |
| throw new ApiException('INVALID_CONFIG_VALUE', 'Fee values must be non-negative numbers.', [], 422); | |
| } | |
| if (str_contains($key, 'days') && (! is_int($value) || $value < 0)) { | |
| throw new ApiException('INVALID_CONFIG_VALUE', 'Day values must be non-negative integers.', [], 422); | |
| } | |
| if (str_contains($key, 'minutes') && (! is_int($value) || $value < 1)) { | |
| throw new ApiException('INVALID_CONFIG_VALUE', 'Minute values must be positive integers.', [], 422); | |
| } | |
| } | |
| } | |