Lines 92.45% 98 / 106
Functions and Methods 66.66% 6 / 9
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
AppointmentUpdateService 92.45% 98 / 106 66.66% 6 / 9 61.55 0.00% 0 / 1
 processUpdate 100.00% 10 / 10 100.00% 1 / 1 2
 validateClientData 89.18% 33 / 37 0.00% 0 / 1 10.13
 extractClientData 100.00% 20 / 20 100.00% 1 / 1 21
 isRebookingUpdate 100.00% 2 / 2 100.00% 1 / 1 2
 loadSourceProcessForRebooking 84.61% 11 / 13 0.00% 0 / 1 4.06
 getReservedProcess 100.00% 1 / 1 100.00% 1 / 1 1
 updateProcessWithClientData 100.00% 12 / 12 100.00% 1 / 1 11
 writeFamilyNameIfAllowed 100.00% 4 / 4 100.00% 1 / 1 4
 saveProcessUpdate 71.42% 5 / 7 0.00% 0 / 1 5.58
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Services\Appointment;
6
7use BO\Zmscitizenapi\Models\AuthenticatedUser;
8use BO\Zmscitizenapi\Models\ThinnedProcess;
9use BO\Zmscitizenapi\Services\Core\ValidationService;
10use BO\Zmscitizenapi\Services\Core\ZmsApiFacadeService;
11use BO\Zmscitizenapi\Services\Core\MapperService;
12use BO\Zmsentities\Helper\ProcessPlainText;
13
14class AppointmentUpdateService
15{
16    public function processUpdate(array $body, ?AuthenticatedUser $authenticatedUser): ThinnedProcess|array
17    {
18        $clientData = $this->extractClientData($body);
19
20        $validated = $this->validateClientData($clientData, $authenticatedUser);
21        if (!$validated instanceof ThinnedProcess) {
22            return $validated;
23        }
24
25        $updatedProcess = $this->updateProcessWithClientData(
26            $validated,
27            $clientData,
28            $this->isRebookingUpdate($clientData)
29        );
30        return $this->saveProcessUpdate($updatedProcess, $authenticatedUser);
31    }
32
33    /**
34     * @return ThinnedProcess|array{errors: array}
35     */
36    private function validateClientData(object $data, ?AuthenticatedUser $authenticatedUser): ThinnedProcess|array
37    {
38        $authErrors = ValidationService::validateGetProcessById($data->processId, $data->authKey);
39        if ($authErrors['errors'] !== []) {
40            return ['errors' => $authErrors['errors']];
41        }
42
43        $reservedProcess = $this->getReservedProcess($data->processId, $data->authKey, $authenticatedUser);
44        if (!$reservedProcess instanceof ThinnedProcess) {
45            $errors = array_key_exists('errors', $reservedProcess) && is_array($reservedProcess['errors'])
46                ? $reservedProcess['errors']
47                : [];
48            return ['errors' => $errors];
49        }
50
51        $statusErrors = ValidationService::validateAppointmentReservedStatus($reservedProcess);
52        if ($statusErrors['errors'] !== []) {
53            return ['errors' => $statusErrors['errors']];
54        }
55
56        if ($this->isRebookingUpdate($data)) {
57            $sourceProcess = $this->loadSourceProcessForRebooking($data);
58            if (is_array($sourceProcess)) {
59                return $sourceProcess;
60            }
61
62            $lockErrors = ValidationService::validateUnchangedStoredContact(
63                $reservedProcess,
64                $data->familyName,
65                $data->email,
66                $data->telephone,
67                $data->customTextfield,
68                $data->customTextfield2
69            );
70            if ($lockErrors['errors'] !== []) {
71                return ['errors' => $lockErrors['errors']];
72            }
73        }
74
75        $fieldErrors = ValidationService::validateAppointmentUpdateFields(
76            $data->familyName,
77            $data->email,
78            $data->telephone,
79            $data->customTextfield,
80            $data->customTextfield2,
81            $reservedProcess->scope ?? null
82        );
83        if ($fieldErrors['errors'] !== []) {
84            return ['errors' => $fieldErrors['errors']];
85        }
86
87        return $reservedProcess;
88    }
89
90    private function extractClientData(array $body): object
91    {
92        return (object) [
93            'processId' => isset($body['processId']) && is_numeric($body['processId'])
94                ? (int) $body['processId']
95                : null,
96            'authKey' => isset($body['authKey']) && is_string($body['authKey']) && trim($body['authKey']) !== ''
97                ? htmlspecialchars(trim($body['authKey']), ENT_QUOTES, 'UTF-8')
98                : null,
99            'familyName' => isset($body['familyName']) && is_string($body['familyName']) ? $body['familyName'] : null,
100            'email' => isset($body['email']) && is_string($body['email']) ? $body['email'] : null,
101            'telephone' => isset($body['telephone']) && is_string($body['telephone']) ? $body['telephone'] : null,
102            'customTextfield' => isset($body['customTextfield']) && is_string($body['customTextfield']) ? $body['customTextfield'] : null,
103            'customTextfield2' => isset($body['customTextfield2']) && is_string($body['customTextfield2']) ? $body['customTextfield2'] : null,
104            'sourceProcessId' => isset($body['sourceProcessId']) && is_numeric($body['sourceProcessId'])
105                ? (int) $body['sourceProcessId']
106                : null,
107            'sourceAuthKey' => isset($body['sourceAuthKey']) && is_string($body['sourceAuthKey'])
108                && trim($body['sourceAuthKey']) !== ''
109                ? htmlspecialchars(trim($body['sourceAuthKey']), ENT_QUOTES, 'UTF-8')
110                : null,
111        ];
112    }
113
114    private function isRebookingUpdate(object $data): bool
115    {
116        return ($data->sourceProcessId ?? null) !== null
117            && ($data->sourceAuthKey ?? null) !== null;
118    }
119
120    private function loadSourceProcessForRebooking(object $clientData): ThinnedProcess|array|null
121    {
122        if (!$this->isRebookingUpdate($clientData)) {
123            return null;
124        }
125
126        $sourceProcess = ZmsApiFacadeService::getThinnedProcessById(
127            $clientData->sourceProcessId,
128            $clientData->sourceAuthKey,
129            null
130        );
131        if (!$sourceProcess instanceof ThinnedProcess) {
132            return $sourceProcess;
133        }
134
135        $rebookingErrors = ValidationService::validateRebookingAllowed($sourceProcess->scope ?? null);
136        if ($rebookingErrors['errors'] !== []) {
137            return $rebookingErrors;
138        }
139
140        return $sourceProcess;
141    }
142
143    private function getReservedProcess(int $processId, ?string $authKey, ?AuthenticatedUser $user): ThinnedProcess|array
144    {
145        return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey, $user);
146    }
147
148    private function updateProcessWithClientData(
149        ThinnedProcess $process,
150        object $data,
151        bool $lockStoredContact = false
152    ): ThinnedProcess {
153        $this->writeFamilyNameIfAllowed($process, $data, $lockStoredContact);
154        if (!$lockStoredContact || !ValidationService::isFilledEmail($process->email)) {
155            $process->email = $data->email ?? $process->email ?? null;
156        }
157        if (!$lockStoredContact || !ValidationService::isFilledContactValue($process->telephone)) {
158            $process->telephone = $data->telephone ?? $process->telephone ?? null;
159        }
160        if (
161            $data->customTextfield !== null
162            && (!$lockStoredContact || !ValidationService::isFilledContactValue($process->customTextfield))
163        ) {
164            $process->customTextfield = ProcessPlainText::normalize($data->customTextfield);
165        }
166        if (
167            $data->customTextfield2 !== null
168            && (!$lockStoredContact || !ValidationService::isFilledContactValue($process->customTextfield2))
169        ) {
170            $process->customTextfield2 = ProcessPlainText::normalize($data->customTextfield2);
171        }
172        return $process;
173    }
174
175    private function writeFamilyNameIfAllowed(
176        ThinnedProcess $process,
177        object $data,
178        bool $lockStoredContact
179    ): void {
180        if (
181            !$lockStoredContact
182            || !ValidationService::isFilledContactValue($process->familyName)
183            || ValidationService::isSameOrCompletedFamilyName($process->familyName, $data->familyName ?? null)
184        ) {
185            $process->familyName = $data->familyName ?? $process->familyName ?? null;
186        }
187    }
188
189    private function saveProcessUpdate(ThinnedProcess $process, ?AuthenticatedUser $authenticatedUser): ThinnedProcess|array
190    {
191        $processEntity = MapperService::thinnedProcessToProcess($process);
192        if (!is_null($authenticatedUser) && is_null($processEntity->getExternalUserId())) {
193            $processEntity->setExternalUserId($authenticatedUser->getExternalUserId());
194        }
195        $result = ZmsApiFacadeService::updateClientData($processEntity);
196        if (is_array($result) && !empty($result['errors'])) {
197            return $result;
198        }
199
200        return MapperService::processToThinnedProcess($result);
201    }
202}