Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.64% covered (warning)
73.64%
81 / 110
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AppointmentReserveService
73.64% covered (warning)
73.64%
81 / 110
14.29% covered (danger)
14.29%
1 / 7
64.46
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processReservation
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
5
 extractClientData
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
10.60
 loadSourceProcessForRebooking
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
6.80
 findMatchingProcess
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 reserveAppointment
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 copySourceContactOntoReservedProcess
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
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\MapperService;
12use BO\Zmscitizenapi\Services\Core\ValidationService;
13use BO\Zmscitizenapi\Services\Core\ZmsApiFacadeService;
14use BO\Zmsentities\Process;
15
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}