tenantRepository->list(); } public function listPaged(array $options): array { return $this->tenantRepository->listPaged($options); } public function findByUuid(string $uuid): ?array { return $this->tenantRepository->findByUuid($uuid); } public function findById(int $id): ?array { return $this->tenantRepository->find($id); } public function createFromAdmin(array $input, int $currentUserId = 0): array { $form = $this->sanitizeBase($input); $form['status'] = $this->normalizeStatus($form['status'] ?? ''); $errors = $this->validateBase($form); if ($errors) { return ['ok' => false, 'errors' => $errors, 'form' => $form]; } // Always UTC — consistency across server timezone changes. $statusChangedAt = gmdate('Y-m-d H:i:s'); $createdId = $this->tenantRepository->create([ 'description' => $form['description'], 'address' => $form['address'], 'postal_code' => $form['postal_code'], 'city' => $form['city'], 'country' => $form['country'], 'region' => $form['region'], 'vat_id' => $form['vat_id'], 'tax_number' => $form['tax_number'], 'phone' => $form['phone'], 'fax' => $form['fax'], 'email' => $form['email'], 'support_email' => $form['support_email'], 'support_phone' => $form['support_phone'], 'billing_email' => $form['billing_email'], 'website' => $form['website'], 'privacy_url' => $form['privacy_url'], 'imprint_url' => $form['imprint_url'], // null means "inherit from global settings" for color, theme, and theme-toggle policy. 'primary_color' => $form['primary_color_use_default'] ? null : ($form['primary_color'] !== '' ? $form['primary_color'] : null), 'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null, 'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'], 'status' => $form['status'], 'status_changed_at' => $statusChangedAt, 'status_changed_by' => $currentUserId > 0 ? $currentUserId : null, 'created_by' => $currentUserId > 0 ? $currentUserId : null, ]); if (!$createdId) { return ['ok' => false, 'errors' => [t('Tenant can not be created')], 'form' => $form]; } $createdTenant = $this->tenantRepository->find((int) $createdId); $uuid = $createdTenant['uuid'] ?? null; if (!empty($input['is_default'])) { $this->settingsGateway->setDefaultTenantId((int) $createdId); } $this->systemAuditService->record('admin.tenants.create', SystemAuditOutcome::Success->value, [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'tenant', 'target_id' => (int) $createdId, 'target_uuid' => is_string($uuid) ? $uuid : '', 'metadata' => ['status' => $form['status']], ]); return ['ok' => true, 'form' => $form, 'uuid' => $uuid, 'id' => (int) $createdId]; } public function updateFromAdmin(int $tenantId, array $input, int $currentUserId = 0): array { $form = $this->sanitizeBase($input); $form['status'] = $this->normalizeStatus($form['status'] ?? ''); $errors = $this->validateBase($form); if ($errors) { return ['ok' => false, 'errors' => $errors, 'form' => $form]; } $existing = $this->tenantRepository->find($tenantId); $statusChanged = false; if ($existing && isset($existing['status'])) { $statusChanged = (string) $existing['status'] !== $form['status']; } $updateData = [ 'description' => $form['description'], 'address' => $form['address'], 'postal_code' => $form['postal_code'], 'city' => $form['city'], 'country' => $form['country'], 'region' => $form['region'], 'vat_id' => $form['vat_id'], 'tax_number' => $form['tax_number'], 'phone' => $form['phone'], 'fax' => $form['fax'], 'email' => $form['email'], 'support_email' => $form['support_email'], 'support_phone' => $form['support_phone'], 'billing_email' => $form['billing_email'], 'website' => $form['website'], 'privacy_url' => $form['privacy_url'], 'imprint_url' => $form['imprint_url'], 'primary_color' => $form['primary_color_use_default'] ? null : ($form['primary_color'] !== '' ? $form['primary_color'] : null), 'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null, 'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'], 'status' => $form['status'], 'modified_by' => $currentUserId > 0 ? $currentUserId : null, ]; if ($statusChanged) { $updateData['status_changed_at'] = gmdate('Y-m-d H:i:s'); $updateData['status_changed_by'] = $currentUserId > 0 ? $currentUserId : null; } $updated = $this->tenantRepository->update($tenantId, $updateData); if (!$updated) { return ['ok' => false, 'errors' => [t('Tenant can not be updated')], 'form' => $form]; } $this->systemAuditService->record('admin.tenants.update', SystemAuditOutcome::Success->value, [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'tenant', 'target_id' => $tenantId, 'target_uuid' => (string) ($existing['uuid'] ?? ''), 'before' => [ 'status' => $existing['status'] ?? null, ], 'after' => [ 'status' => $form['status'], ], ]); return ['ok' => true, 'form' => $form]; } public function deleteByUuid(string $uuid): array { $uuid = trim($uuid); if ($uuid === '') { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $tenant = $this->tenantRepository->findByUuid($uuid); if (!$tenant || !isset($tenant['id'])) { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $tenantId = (int) $tenant['id']; // Block deletion if departments exist — orphaned departments would break user assignments. if ($this->departmentRepository->countByTenantId($tenantId) > 0) { return ['ok' => false, 'status' => 409, 'error' => 'tenant_has_departments']; } $deleted = $this->tenantRepository->delete($tenantId); if (!$deleted) { return ['ok' => false, 'status' => 500, 'error' => 'delete_failed']; } $this->systemAuditService->record('admin.tenants.delete', SystemAuditOutcome::Success->value, [ 'target_type' => 'tenant', 'target_id' => $tenantId, 'target_uuid' => (string) ($tenant['uuid'] ?? ''), ]); return ['ok' => true, 'tenant' => $tenant]; } private function sanitizeBase(array $input): array { return [ 'description' => trim((string) ($input['description'] ?? '')), 'address' => trim((string) ($input['address'] ?? '')), 'postal_code' => trim((string) ($input['postal_code'] ?? '')), 'city' => trim((string) ($input['city'] ?? '')), 'country' => trim((string) ($input['country'] ?? '')), 'region' => trim((string) ($input['region'] ?? '')), 'vat_id' => trim((string) ($input['vat_id'] ?? '')), 'tax_number' => trim((string) ($input['tax_number'] ?? '')), 'phone' => trim((string) ($input['phone'] ?? '')), 'fax' => trim((string) ($input['fax'] ?? '')), 'email' => trim((string) ($input['email'] ?? '')), 'support_email' => trim((string) ($input['support_email'] ?? '')), 'support_phone' => trim((string) ($input['support_phone'] ?? '')), 'billing_email' => trim((string) ($input['billing_email'] ?? '')), 'website' => trim((string) ($input['website'] ?? '')), 'privacy_url' => trim((string) ($input['privacy_url'] ?? '')), 'imprint_url' => trim((string) ($input['imprint_url'] ?? '')), 'primary_color' => trim((string) ($input['primary_color'] ?? '')), 'primary_color_use_default' => !empty($input['primary_color_use_default']), 'default_theme' => strtolower(trim((string) ($input['default_theme'] ?? ''))), 'allow_user_theme_mode' => trim((string) ($input['allow_user_theme_mode'] ?? '')), 'status' => trim((string) ($input['status'] ?? '')), ]; } private function validateBase(array $form): array { $errors = []; if ($form['description'] === '') { $errors[] = t('Description cannot be empty'); } if (TenantStatus::tryNormalize((string) $form['status']) === null) { $errors[] = t('Status is invalid'); } if ( !$form['primary_color_use_default'] && $form['primary_color'] !== '' && !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $form['primary_color']) ) { $errors[] = t('Primary color is invalid'); } $themes = function_exists('appThemes') ? appThemes() : []; if ($form['default_theme'] !== '' && !isset($themes[$form['default_theme']])) { $errors[] = t('Default theme is invalid'); } if (!in_array($form['allow_user_theme_mode'], ['', '0', '1'], true)) { $errors[] = t('User theme policy is invalid'); } return $errors; } private function normalizeStatus(string $status): string { return TenantStatus::normalizeOr($status, TenantStatus::Active)->value; } }