[self::STATUS_DRAFT, self::STATUS_IN_PROGRESS, self::STATUS_COMPLETED], self::PERMISSION_MANAGE => [self::STATUS_DRAFT, self::STATUS_IN_PROGRESS, self::STATUS_COMPLETED, self::STATUS_ARCHIVED], ]; /** * Statuses in which a create-level user may edit field values. */ private const EDITABLE_STATUSES_CREATE = [self::STATUS_DRAFT, self::STATUS_IN_PROGRESS]; public function __construct( private readonly HandoverRepository $repository, private readonly SoftwareProductService $softwareProductService, ) { } /** * Create a new handover with a schema snapshot from the software product. * * @return array{ok: bool, id: int|null, errors: array} */ /** * @param array $fieldValues */ public function create( int $tenantId, string $debitorNo, string $debitorName, string $productCode, int $userId, array $fieldValues = [], ): array { $errors = []; $debitorNo = trim($debitorNo); if ($debitorNo === '') { $errors['debitor_no'] = t('Debitor number is required'); } $productCode = trim($productCode); if ($productCode === '') { $errors['product_code'] = t('Software product is required'); } if ($errors !== []) { return ['ok' => false, 'id' => null, 'errors' => $errors]; } $product = $this->softwareProductService->findByCode($productCode); if ($product === null) { return ['ok' => false, 'id' => null, 'errors' => ['product_code' => t('Software product not found')]]; } $schemaRaw = $product['handover_protocol_schema'] ?? null; if ($schemaRaw === null || $schemaRaw === '') { return ['ok' => false, 'id' => null, 'errors' => ['product_code' => t('Software product has no handover protocol schema')]]; } $schema = json_decode($schemaRaw, true); if (!is_array($schema) || empty($schema['fields'])) { return ['ok' => false, 'id' => null, 'errors' => ['product_code' => t('Software product has no handover protocol schema')]]; } // Validate field values against schema if provided if ($fieldValues !== []) { $validationErrors = $this->validateFieldValues($fieldValues, $schema['fields']); if ($validationErrors !== []) { return ['ok' => false, 'id' => null, 'errors' => $validationErrors]; } } $fieldValuesJson = $fieldValues !== [] ? json_encode($fieldValues, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : json_encode(new \stdClass(), JSON_FORCE_OBJECT); $id = $this->repository->insert([ 'tenant_id' => $tenantId, 'debitor_no' => $debitorNo, 'debitor_name' => trim($debitorName), 'product_code' => $productCode, 'status' => self::STATUS_DRAFT, 'schema_snapshot' => $schemaRaw, 'field_values' => $fieldValuesJson, 'created_by' => $userId, ]); if ($id === null) { return ['ok' => false, 'id' => null, 'errors' => ['general' => t('Failed to create handover')]]; } return ['ok' => true, 'id' => $id, 'errors' => []]; } /** * Update field values for a handover. * * @param array $fieldValues * @return array{ok: bool, errors: array} */ public function updateFields( int $tenantId, int $id, array $fieldValues, int $userId, string $permissionLevel, ): array { $handover = $this->repository->findById($tenantId, $id); if ($handover === null) { return ['ok' => false, 'errors' => ['general' => t('Handover not found')]]; } if (!$this->canEdit($handover['status'], $permissionLevel)) { return ['ok' => false, 'errors' => ['general' => t('You do not have permission to edit this handover')]]; } $schema = json_decode((string) ($handover['schema_snapshot'] ?? '{}'), true); $schemaFields = is_array($schema) ? ($schema['fields'] ?? []) : []; $validationErrors = $this->validateFieldValues($fieldValues, $schemaFields); if ($validationErrors !== []) { return ['ok' => false, 'errors' => $validationErrors]; } $json = json_encode($fieldValues, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $updated = $this->repository->updateFieldValues($tenantId, $id, $json, $userId); if (!$updated) { return ['ok' => false, 'errors' => ['general' => t('Failed to save')]]; } return ['ok' => true, 'errors' => []]; } /** * Change the status of a handover. * * @return array{ok: bool, errors: array} */ public function changeStatus( int $tenantId, int $id, string $newStatus, int $userId, string $permissionLevel, ): array { if (!in_array($newStatus, self::ALL_STATUSES, true)) { return ['ok' => false, 'errors' => ['status' => t('Invalid status')]]; } $handover = $this->repository->findById($tenantId, $id); if ($handover === null) { return ['ok' => false, 'errors' => ['general' => t('Handover not found')]]; } $allowed = self::ALLOWED_STATUS_BY_PERMISSION[$permissionLevel] ?? []; if (!in_array($newStatus, $allowed, true)) { return ['ok' => false, 'errors' => ['status' => t('You do not have permission to set this status')]]; } $updated = $this->repository->updateStatus($tenantId, $id, $newStatus, $userId); if (!$updated) { return ['ok' => false, 'errors' => ['general' => t('Failed to update status')]]; } return ['ok' => true, 'errors' => []]; } /** * @return array{total: int, rows: list>} */ public function listPaged(int $tenantId, array $filters): array { return $this->repository->listPaged($tenantId, $filters); } public function findById(int $tenantId, int $id): ?array { return $this->repository->findById($tenantId, $id); } /** * Check if a user with the given permission level can edit a handover in the given status. */ public function canEdit(string $status, string $permissionLevel): bool { if ($permissionLevel === self::PERMISSION_MANAGE) { return true; } if ($permissionLevel === self::PERMISSION_CREATE) { return in_array($status, self::EDITABLE_STATUSES_CREATE, true); } return false; } /** * Get the list of statuses a user with the given permission level can set. * * @return list */ public function getAllowedStatuses(string $permissionLevel): array { return self::ALLOWED_STATUS_BY_PERMISSION[$permissionLevel] ?? []; } /** * Validate field values against the schema snapshot. * * @param array $fieldValues * @param list> $schemaFields * @return array */ private function validateFieldValues(array $fieldValues, array $schemaFields): array { $errors = []; $allowedKeys = []; $requiredKeys = []; foreach ($schemaFields as $field) { $key = $field['key'] ?? null; if ($key === null) { continue; } $allowedKeys[$key] = true; if (!empty($field['required'])) { $requiredKeys[$key] = $field['label'] ?? $key; } } foreach ($fieldValues as $key => $value) { if (!isset($allowedKeys[$key])) { $errors[$key] = t('Unknown field: %s', $key); } } foreach ($requiredKeys as $key => $label) { $value = $fieldValues[$key] ?? ''; if (is_string($value) && trim($value) === '') { $errors[$key] = t('%s is required', $label); } } return $errors; } /** * Get the human-readable label for a status. */ public static function statusLabel(string $status): string { return match ($status) { self::STATUS_DRAFT => t('Draft'), self::STATUS_IN_PROGRESS => t('In progress'), self::STATUS_COMPLETED => t('Completed'), self::STATUS_ARCHIVED => t('Archived'), default => $status, }; } /** * Get the badge variant for a status. */ public static function statusVariant(string $status): string { return match ($status) { self::STATUS_DRAFT => 'neutral', self::STATUS_IN_PROGRESS => 'warning', self::STATUS_COMPLETED => 'success', self::STATUS_ARCHIVED => 'neutral', default => 'neutral', }; } }