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