Lines 76.72% 89 / 116
Methods 44.44% 4 / 9
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 processReservation 95.00% 38 / 40 0.00% 0 / 1 5
 extractClientData 100.00% 13 / 13 100.00% 1 / 1 10
 loadSourceProcessForRebooking 25.00% 2 / 8 0.00% 0 / 1 6.80
 findMatchingProcess 88.23% 15 / 17 0.00% 0 / 1 8.10
 reserveAppointment 88.23% 15 / 17 0.00% 0 / 1 8.10
 copySourceContactOntoReservedProcess 0.00% 0 / 15 0.00% 0 / 1 12
 [BO\Zmscitizenapi\Services\Captcha\CaptchaRequirementTrait] isCaptchaRequiredForOfficeIds 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmscitizenapi\Services\Captcha\CaptchaRequirementTrait] isCaptchaRequiredForOfficeId 100.00% 3 / 3 100.00% 1 / 1 3
16class AppointmentReserveService
17{
18    use CaptchaRequirementTrait;
19
20    private TokenValidationService $tokenValidator;
21    private ZmsApiFacadeService $zmsApiFacadeService;
22
23    public function __construct()
24    {
25        $this->tokenValidator = new TokenValidationService();
26        $this->zmsApiFacadeService = new ZmsApiFacadeService();
27    }
28
29    public function processReservation(array $body, bool $showUnpublished = false): ThinnedProcess|array
30    {
31        $clientData = $this->extractClientData($body);
32
33        $captchaRequired = $this->isCaptchaRequiredForOfficeId($clientData->officeId);
34        $captchaToken = $body['captchaToken'] ?? null;
35
36        $errors = ValidationService::validatePostAppointmentReserve(
37            $clientData->officeId,
38            $clientData->serviceIds,
39            $clientData->serviceCounts,
40            $clientData->timestamp,
41            $captchaRequired,
42            $captchaToken,
43            $this->tokenValidator
44        );
45        if (!empty($errors['errors'])) {
46            return $errors;
47        }
48
49        $errors = ValidationService::validateServiceLocationCombination(
50            $clientData->officeId,
51            $clientData->serviceIds,
52            $showUnpublished
53        );
54        if (!empty($errors['errors'])) {
55            return $errors;
56        }
57
58        $selectedProcess = $this->findMatchingProcess(
59            $clientData->officeId,
60            $clientData->serviceIds,
61            $clientData->serviceCounts,
62            $clientData->timestamp
63        );
64
65        $errors = ValidationService::validateGetProcessNotFound($selectedProcess);
66        if (!empty($errors['errors'])) {
67            return $errors;
68        }
69
70        $sourceProcess = $this->loadSourceProcessForRebooking($clientData);
71        if (is_array($sourceProcess)) {
72            return $sourceProcess;
73        }
74
75        return $this->reserveAppointment(
76            $selectedProcess,
77            $clientData->serviceIds,
78            $clientData->serviceCounts,
79            $clientData->officeId,
80            $sourceProcess
81        );
82    }
83
84    private function extractClientData(array $body): object
85    {
86        return (object) [
87            'officeId' => isset($body['officeId']) && is_numeric($body['officeId']) ? (int) $body['officeId'] : null,
88            'serviceIds' => $body['serviceId'] ?? null,
89            'serviceCounts' => $body['serviceCount'] ?? [1],
90            'timestamp' => isset($body['timestamp']) && is_numeric($body['timestamp']) ? (int) $body['timestamp'] : null,
91            'sourceProcessId' => isset($body['sourceProcessId']) && is_numeric($body['sourceProcessId'])
92                ? (int) $body['sourceProcessId']
93                : null,
94            'sourceAuthKey' => isset($body['sourceAuthKey']) && is_string($body['sourceAuthKey'])
95                && trim($body['sourceAuthKey']) !== ''
96                ? htmlspecialchars(trim($body['sourceAuthKey']), ENT_QUOTES, 'UTF-8')
97                : null,
98        ];
99    }
100
101    /**
102     * @return ThinnedProcess|array{errors: array}|null
103     */
104    private function loadSourceProcessForRebooking(object $clientData): ThinnedProcess|array|null
105    {
106        if ($clientData->sourceProcessId === null || $clientData->sourceAuthKey === null) {
107            return null;
108        }
109        $sourceProcess = ZmsApiFacadeService::getThinnedProcessById(
110            $clientData->sourceProcessId,
111            $clientData->sourceAuthKey,
112            null
113        );
114        return $sourceProcess;
115    }
116
117    private function findMatchingProcess(int $officeId, array $serviceIds, array $serviceCounts, int $timestamp): ?Process
118    {
119        $freeAppointments = ZmsApiFacadeService::getFreeAppointments($officeId, $serviceIds, $serviceCounts, DateTimeFormatHelper::getInternalDateFromTimestamp($timestamp));
120        foreach ($freeAppointments as $process) {
121            if (!isset($process->appointments) || empty($process->appointments)) {
122                continue;
123            }
124
125            foreach ($process->appointments as $appointment) {
126                if ((int) $appointment->date === $timestamp) {
127                    $requestIds = [];
128                    if ($process->requests) {
129                        foreach ($process->requests as $request) {
130                            $requestIds[] = $request->getId();
131                        }
132                    }
133
134                    $processData = [
135                        'requests' => $requestIds,
136                        'appointments' => [$appointment]
137                    ];
138                    $process->withUpdatedData($processData, new \DateTime("@$timestamp"), $process->scope);
139                    return $process;
140                }
141            }
142        }
143
144        return null;
145    }
146
147    private function reserveAppointment(
148        Process $process,
149        array $serviceIds,
150        array $serviceCounts,
151        int $officeId,
152        ?ThinnedProcess $sourceProcess
153    ): ThinnedProcess|array {
154        $process->clients = [
155            [
156                'email' => \App::getPlaceholderEmail()
157            ]
158        ];
159        $reservedProcess = ZmsApiFacadeService::reserveTimeslot($process, $serviceIds, $serviceCounts);
160        if (is_array($reservedProcess)) {
161            return $reservedProcess;
162        }
163        if ($reservedProcess && $reservedProcess->scope && $reservedProcess->scope->id) {
164            $scopeId = $reservedProcess->scope->id;
165            $scope = ZmsApiFacadeService::getScopeById((int) $scopeId);
166            if (!is_array($scope)) {
167                $reservedProcess->scope = $scope;
168                $reservedProcess->officeId = $officeId;
169            }
170        }
171
172        if ($sourceProcess instanceof ThinnedProcess && $reservedProcess instanceof ThinnedProcess) {
173            return $this->copySourceContactOntoReservedProcess($reservedProcess, $sourceProcess);
174        }
175
176        return $reservedProcess;
177    }
178
179    private function copySourceContactOntoReservedProcess(
180        ThinnedProcess $reservedProcess,
181        ThinnedProcess $sourceProcess
182    ): ThinnedProcess|array {
183        $reservedProcess->familyName = $sourceProcess->familyName;
184        if (ValidationService::isFilledEmail($sourceProcess->email)) {
185            $reservedProcess->email = $sourceProcess->email;
186        }
187        $reservedProcess->telephone = $sourceProcess->telephone;
188        $reservedProcess->customTextfield = $sourceProcess->customTextfield;
189        $reservedProcess->customTextfield2 = $sourceProcess->customTextfield2;
190
191        $updatedProcess = ZmsApiFacadeService::updateClientData(
192            MapperService::thinnedProcessToProcess($reservedProcess)
193        );
194        if (is_array($updatedProcess)) {
195            return $updatedProcess;
196        }
197
198        $copiedProcess = MapperService::processToThinnedProcess($updatedProcess);
199        $copiedProcess->scope = $reservedProcess->scope;
200        $copiedProcess->officeId = $reservedProcess->officeId;
201        return $copiedProcess;
202    }
203}

From BO\Zmscitizenapi\Services\Captcha\CaptchaRequirementTrait

10trait CaptchaRequirementTrait
11{
12    private function isCaptchaRequiredForOfficeIds(array $officeIds): bool
13    {
14        return $this->zmsApiFacadeService->isCaptchaRequiredForAnyOffice($officeIds);
15    }
16
17    private function isCaptchaRequiredForOfficeId(?int $officeId): bool
18    {
19        if ($officeId === null || $officeId <= 0) {
20            return false;
21        }
22
23        return $this->isCaptchaRequiredForOfficeIds([(string) $officeId]);
24    }
25}