Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.52% covered (success)
91.52%
302 / 330
14.29% covered (danger)
14.29%
3 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZmsApiFacadeService
91.52% covered (success)
91.52%
302 / 330
14.29% covered (danger)
14.29%
3 / 21
131.09
0.00% covered (danger)
0.00%
0 / 1
 setMappedCache
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getOffices
96.23% covered (success)
96.23%
51 / 53
0.00% covered (danger)
0.00%
0 / 1
18
 getScopes
97.67% covered (success)
97.67%
42 / 43
0.00% covered (danger)
0.00%
0 / 1
14
 getServices
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
8.15
 getServicesAndOffices
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 isCaptchaRequiredForAnyOffice
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
11.24
 getScopeById
94.87% covered (success)
94.87%
37 / 39
0.00% covered (danger)
0.00%
0 / 1
12.02
 getServicesByOfficeId
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
13.08
 getCalendarAvailability
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
10
 getFreeAppointments
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
7.02
 reserveTimeslot
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getProcessById
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 getThinnedProcessById
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 updateClientData
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 preconfirmAppointment
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 confirmAppointment
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 cancelAppointment
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 sendPreconfirmationEmail
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 sendConfirmationEmail
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 sendCancellationEmail
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getAppointmentsByExternalUserId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Services\Core;
6
7use BO\Slim\LoggerService;
8use BO\Zmscitizenapi\Exceptions\UnauthorizedException;
9use BO\Zmscitizenapi\Models\AuthenticatedUser;
10use BO\Zmscitizenapi\Models\AvailableCalendar;
11use BO\Zmscitizenapi\Models\Office;
12use BO\Zmscitizenapi\Models\Service;
13use BO\Zmscitizenapi\Models\ThinnedProcess;
14use BO\Zmscitizenapi\Models\ThinnedScope;
15use BO\Zmscitizenapi\Models\Collections\OfficeList;
16use BO\Zmscitizenapi\Models\Collections\OfficeServiceRelationList;
17use BO\Zmscitizenapi\Models\Collections\OfficeServiceAndRelationList;
18use BO\Zmscitizenapi\Models\Collections\ServiceList;
19use BO\Zmscitizenapi\Models\Collections\ThinnedScopeList;
20use BO\Zmsentities\Collection\RequestRelationList;
21use BO\Zmsentities\Process;
22use BO\Zmsentities\Scope;
23use BO\Zmsentities\Collection\ScopeList;
24use BO\Zmsentities\Collection\ProviderList;
25use BO\Zmsentities\Collection\RequestList;
26use BO\Zmsentities\Collection\ProcessList;
27
28/**
29 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
30 * @TODO: Break down this facade into smaller domain-specific facades or use the Command pattern
31 */
32class ZmsApiFacadeService
33{
34    private const CACHE_KEY_OFFICES = 'processed_offices';
35    private const CACHE_KEY_SCOPES = 'processed_scopes';
36    private const CACHE_KEY_SERVICES = 'processed_services';
37    private const CACHE_KEY_OFFICES_AND_SERVICES = 'processed_offices_and_services';
38    private const CACHE_KEY_SERVICES_BY_OFFICE_PREFIX = 'processed_services_by_office_';
39
40    private static function setMappedCache(string $cacheKey, mixed $data): void
41    {
42        if (\App::$cache) {
43            \App::$cache->set($cacheKey, $data, \App::$SOURCE_CACHE_TTL);
44            LoggerService::logInfo('Second-level cache set', [
45                'key' => $cacheKey,
46                'ttl' => \App::$SOURCE_CACHE_TTL
47            ]);
48        }
49    }
50
51    /**
52     * @SuppressWarnings(PHPMD.NPathComplexity)
53     */
54    public static function getOffices(bool $showUnpublished = false): OfficeList
55    {
56        $cacheKey = self::CACHE_KEY_OFFICES . ($showUnpublished ? '_unpublished' : '');
57
58        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
59            return $cachedData;
60        }
61
62        $providerList = ZmsApiClientService::getOffices();
63        $scopeList = ZmsApiClientService::getScopes();
64        $offices = [];
65        $scopeMap = [];
66        foreach ($scopeList as $scope) {
67            if ($scope->getProvider()) {
68                $scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
69            }
70        }
71
72        foreach ($providerList as $provider) {
73            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
74                continue;
75            }
76
77            $matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
78            $offices[] = new Office(
79                id: (int) $provider->id,
80                name: $provider->displayName ?? $provider->name,
81                address: $provider->data['address'] ?? null,
82                showAlternativeLocations: $provider->data['showAlternativeLocations'] ?? null,
83                displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
84                organization: $provider->data['organization'] ?? null,
85                organizationUnit: $provider->data['organizationUnit'] ?? null,
86                slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
87                geo: $provider->data['geo'] ?? null,
88                scope: $matchingScope ? new ThinnedScope(
89                    id: (int) $matchingScope->id,
90                    provider: MapperService::providerToThinnedProvider($provider),
91                    shortName: (string) $matchingScope->getShortName(),
92                    emailFrom: (string) $matchingScope->getEmailFrom(),
93                    emailRequired: (bool) $matchingScope->getEmailRequired(),
94                    telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
95                    telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
96                    customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
97                    customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
98                    customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
99                    customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated(),
100                    customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required(),
101                    customTextfield2Label: $matchingScope->getCustomTextfield2Label(),
102                    captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
103                    infoForAppointment: $matchingScope->getInfoForAppointment(),
104                    infoForAllAppointments: $matchingScope->getInfoForAllAppointments(),
105                    slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()),
106                    appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()),
107                    whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()),
108                    activationDuration: MapperService::extractActivationDuration($matchingScope),
109                    reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
110                    hint: ($matchingScope && trim((string) $matchingScope->getScopeHint()) !== '')  ? (string) $matchingScope->getScopeHint() : null
111                ) : null,
112                slotsPerAppointment: $matchingScope ? ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) : null
113            );
114        }
115
116        $result = new OfficeList($offices);
117
118        self::setMappedCache($cacheKey, $result);
119
120        return $result;
121    }
122
123    /**
124     * @SuppressWarnings(PHPMD.NPathComplexity)
125     */
126    public static function getScopes(): ThinnedScopeList|array
127    {
128        $cacheKey = self::CACHE_KEY_SCOPES;
129
130        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
131            return $cachedData;
132        }
133
134        $providerList = ZmsApiClientService::getOffices();
135        $scopeList = ZmsApiClientService::getScopes();
136        $scopeMap = [];
137        foreach ($scopeList as $scope) {
138            $scopeProvider = $scope->getProvider();
139            if ($scopeProvider && $scopeProvider->id && $scopeProvider->source) {
140                $key = $scopeProvider->source . '_' . $scopeProvider->id;
141                $scopeMap[$key] = $scope;
142            }
143        }
144
145        $scopesProjectionList = [];
146        foreach ($providerList as $provider) {
147            $key = $provider->source . '_' . $provider->id;
148            if (isset($scopeMap[$key])) {
149                $matchingScope = $scopeMap[$key];
150                $scopesProjectionList[] = new ThinnedScope(
151                    id: (int) $matchingScope->id,
152                    provider: MapperService::providerToThinnedProvider($provider),
153                    shortName: (string) $matchingScope->getShortName(),
154                    emailFrom: (string) $matchingScope->getEmailFrom(),
155                    emailRequired: (bool) $matchingScope->getEmailRequired(),
156                    telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
157                    telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
158                    customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
159                    customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
160                    customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
161                    customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated(),
162                    customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required(),
163                    customTextfield2Label: $matchingScope->getCustomTextfield2Label(),
164                    captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
165                    infoForAppointment: $matchingScope->getInfoForAppointment(),
166                    infoForAllAppointments: $matchingScope->getInfoForAllAppointments(),
167                    slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()),
168                    appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()),
169                    whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()),
170                    reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
171                    activationDuration: MapperService::extractActivationDuration($matchingScope),
172                    hint: ($matchingScope && trim((string) $matchingScope->getScopeHint()) !== '') ? (string) $matchingScope->getScopeHint() : null
173                );
174            }
175        }
176
177        $result = new ThinnedScopeList($scopesProjectionList);
178
179        self::setMappedCache($cacheKey, $result);
180
181        return $result;
182    }
183
184    public static function getServices(bool $showUnpublished = false): ServiceList|array
185    {
186        $cacheKey = self::CACHE_KEY_SERVICES . ($showUnpublished ? '_unpublished' : '');
187
188        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
189            return $cachedData;
190        }
191
192        $requestList = ZmsApiClientService::getServices();
193        $services = [];
194        foreach ($requestList as $request) {
195            $additionalData = $request->getAdditionalData();
196            if (
197                !$showUnpublished
198                && isset($additionalData['public'])
199                && !$additionalData['public']
200            ) {
201                continue;
202            }
203
204            $services[] = new Service(id: (int) $request->getId(), name: $request->getName(), maxQuantity: $additionalData['maxQuantity'] ?? 1);
205        }
206
207        $result = new ServiceList($services);
208
209        self::setMappedCache($cacheKey, $result);
210
211        return $result;
212    }
213
214    public static function getServicesAndOffices(bool $showUnpublished = false): OfficeServiceAndRelationList|array
215    {
216        $cacheKey = self::CACHE_KEY_OFFICES_AND_SERVICES . ($showUnpublished ? '_unpublished' : '');
217
218        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
219            return $cachedData;
220        }
221
222        $providerList = ZmsApiClientService::getOffices();
223        $requestList = ZmsApiClientService::getServices();
224        $relationList = ZmsApiClientService::getRequestRelationList();
225
226        $offices = MapperService::mapOfficesWithScope($providerList, $showUnpublished);
227        $services = MapperService::mapServicesWithCombinations(
228            $requestList,
229            $relationList,
230            $showUnpublished
231        );
232        $relations = MapperService::mapRelations($relationList, $showUnpublished);
233
234        $result = new OfficeServiceAndRelationList($offices, $services, $relations);
235
236        self::setMappedCache($cacheKey, $result);
237
238        return $result;
239    }
240
241    /**
242     * One offices + scopes load, then in-memory captcha checks for all office IDs.
243     * Avoids per-office scope reloads that each rebuild a full ThinnedScope.
244     */
245    public static function isCaptchaRequiredForAnyOffice(array $officeIds): bool
246    {
247        $wanted = [];
248        foreach ($officeIds as $officeIdRaw) {
249            $officeId = (int) $officeIdRaw;
250            if ($officeId > 0) {
251                $wanted[$officeId] = true;
252            }
253        }
254        if ($wanted === []) {
255            return false;
256        }
257
258        $providerById = [];
259        foreach (ZmsApiClientService::getOffices() as $provider) {
260            $providerById[(int) $provider->id] = $provider;
261        }
262
263        $scopes = ZmsApiClientService::getScopes();
264        if (!$scopes instanceof ScopeList) {
265            return false;
266        }
267
268        foreach (array_keys($wanted) as $officeId) {
269            $provider = $providerById[$officeId] ?? null;
270            if ($provider === null) {
271                continue;
272            }
273            $source = (string) ($provider->source ?? '');
274            if ($source === '') {
275                continue;
276            }
277
278            $matchingScope = $scopes->withProviderID($source, (string) $officeId)->getIterator()->current();
279            if ($matchingScope instanceof Scope && $matchingScope->getCaptchaActivatedRequired()) {
280                return true;
281            }
282        }
283
284        return false;
285    }
286
287    /**
288     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
289     * @SuppressWarnings(PHPMD.NPathComplexity)
290     */
291    public static function getScopeById(?int $scopeId): ThinnedScope|array
292    {
293        $scopeList = ZmsApiClientService::getScopes();
294        $matchingScope = null;
295        foreach ($scopeList as $scope) {
296            if ((int) $scope->id === (int) $scopeId) {
297                $matchingScope = $scope;
298                break;
299            }
300        }
301
302        $tempScopeList = new ScopeList();
303        if ($matchingScope !== null) {
304            $tempScopeList->addEntity($matchingScope);
305        }
306        $errors = ValidationService::validateScopesNotFound($tempScopeList);
307        if (is_array($errors) && !empty($errors['errors'])) {
308            return $errors;
309        }
310
311        if ($matchingScope === null) {
312            return ValidationService::validateScopesNotFound(new ScopeList());
313        }
314
315        $scopeProvider = $matchingScope->getProvider();
316        return new ThinnedScope(
317            id: (int) $matchingScope->id,
318            provider: $scopeProvider ? MapperService::providerToThinnedProvider($scopeProvider) : null,
319            shortName: (string) $matchingScope->getShortName(),
320            emailFrom: (string) $matchingScope->getEmailFrom(),
321            emailRequired: (bool) $matchingScope->getEmailRequired(),
322            telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
323            telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
324            customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
325            customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
326            customTextfieldLabel: $matchingScope->getCustomTextfieldLabel() ?? null,
327            customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated(),
328            customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required(),
329            customTextfield2Label: $matchingScope->getCustomTextfield2Label() ?? null,
330            captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
331            infoForAppointment: $matchingScope->getInfoForAppointment() ?? null,
332            infoForAllAppointments: $matchingScope->getInfoForAllAppointments() ?? null,
333            slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()),
334            appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()),
335            whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()),
336            reservationDuration: MapperService::extractReservationDuration($matchingScope),
337            activationDuration: MapperService::extractActivationDuration($matchingScope),
338            hint: ((string) $matchingScope->getScopeHint() === '' ? null : (string) $matchingScope->getScopeHint())
339        );
340    }
341
342    public static function getServicesByOfficeId(int $officeId, bool $showUnpublished = false): ServiceList|array
343    {
344        $cacheKey = self::CACHE_KEY_SERVICES_BY_OFFICE_PREFIX . $officeId . ($showUnpublished ? '_unpublished' : '');
345
346        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
347            return $cachedData;
348        }
349
350        $requestList = ZmsApiClientService::getServices();
351        $requestRelationList = ZmsApiClientService::getRequestRelationList();
352        $requestMap = [];
353        foreach ($requestList as $request) {
354            $additionalData = $request->getAdditionalData();
355            if (
356                !$showUnpublished
357                && isset($additionalData['public'])
358                && !$additionalData['public']
359            ) {
360                continue;
361            }
362
363            $requestMap[$request->id] = $request;
364        }
365
366        $services = [];
367        foreach ($requestRelationList as $relation) {
368            if ((int) $relation->provider->id === $officeId) {
369                $requestId = $relation->request->id;
370                if (isset($requestMap[$requestId])) {
371                    $request = $requestMap[$requestId];
372                    $services[] = new Service(id: (int) $request->id, name: $request->name, maxQuantity: $request->getAdditionalData()['maxQuantity'] ?? 1);
373                }
374            }
375        }
376
377        $errors = ValidationService::validateServicesNotFound($services);
378        if (is_array($errors) && !empty($errors['errors'])) {
379            return $errors;
380        }
381
382        $result = new ServiceList($services);
383
384        self::setMappedCache($cacheKey, $result);
385
386        return $result;
387    }
388
389    public static function getCalendarAvailability(
390        array $officeIds,
391        array $serviceIds,
392        array $serviceCounts,
393        string $startDate,
394        string $endDate,
395        ?string $slotsStartDate = null,
396        ?string $slotsEndDate = null
397    ): AvailableCalendar|array {
398        $params = [
399            'startDate' => $startDate,
400            'endDate' => $endDate,
401            'officeIds' => implode(',', $officeIds),
402            'serviceIds' => implode(',', $serviceIds),
403        ];
404        if ($serviceCounts !== []) {
405            $params['serviceCounts'] = implode(',', $serviceCounts);
406        }
407        if ($slotsStartDate !== null && $slotsStartDate !== '') {
408            $params['slotsStartDate'] = $slotsStartDate;
409        }
410        if ($slotsEndDate !== null && $slotsEndDate !== '') {
411            $params['slotsEndDate'] = $slotsEndDate;
412        }
413
414        $availability = ZmsApiClientService::getCalendarAvailability($params);
415
416        $formattedDays = [];
417        foreach ($availability['days'] ?? [] as $day) {
418            $offices = [];
419            foreach ($day['appointments'] ?? [] as $officeId => $timestamps) {
420                $offices[] = [
421                    'officeId' => (string) $officeId,
422                    'appointments' => array_map('intval', array_values((array) $timestamps)),
423                ];
424            }
425
426            $formattedDays[] = [
427                'date' => (string) ($day['date'] ?? ''),
428                'providerIDs' => (string) ($day['providerIDs'] ?? ''),
429                'offices' => $offices,
430            ];
431        }
432
433        return new AvailableCalendar(
434            (string) ($availability['startDate'] ?? $startDate),
435            (string) ($availability['endDate'] ?? $endDate),
436            $formattedDays,
437            (string) ($availability['slotsStartDate'] ?? $slotsStartDate ?? $startDate),
438            (string) ($availability['slotsEndDate'] ?? $slotsEndDate ?? $endDate),
439            isset($availability['prevBookableDate']) ? (string) $availability['prevBookableDate'] : null,
440            isset($availability['nextBookableDate']) ? (string) $availability['nextBookableDate'] : null
441        );
442    }
443
444    public static function getFreeAppointments(int $officeId, array $serviceIds, array $serviceCounts, array $date): ProcessList|array
445    {
446        $providerList = ZmsApiClientService::getOffices();
447        $requestList  = ZmsApiClientService::getServices();
448
449        $providerSource = [];
450        foreach ($providerList as $p) {
451            $providerSource[(string)$p->id] = (string)($p->source ?? '');
452        }
453        $requestSource  = [];
454        foreach ($requestList as $r) {
455            $requestSource[(string)$r->id] = (string)($r->source ?? '');
456        }
457
458        $oid = (string)$officeId;
459        $provSrc = $providerSource[$oid] ?? null;
460        if (!$provSrc) {
461            return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
462        }
463
464        $office = ['id' => $officeId, 'source' => $provSrc];
465
466        $requests = [];
467        foreach ($serviceIds as $id => $serviceId) {
468            $sid   = (string)$serviceId;
469            $reqSrc = $requestSource[$sid] ?? null;
470            if (!$reqSrc) {
471                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
472            }
473            $count = (int)($serviceCounts[$id] ?? 1);
474            for ($k = 0; $k < $count; $k++) {
475                $requests[] = ['id' => $serviceId, 'source' => $reqSrc];
476            }
477        }
478
479        return ZmsApiClientService::getFreeTimeslots(
480            new ProviderList([$office]),
481            new RequestList($requests),
482            $date,
483            $date
484        );
485    }
486
487    public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): ThinnedProcess|array
488    {
489        $errors = ValidationService::validateServiceArrays($serviceIds, $serviceCounts);
490        if (!empty($errors)) {
491            return $errors;
492        }
493        $process = ZmsApiClientService::reserveTimeslot($appointmentProcess, $serviceIds, $serviceCounts);
494        return MapperService::processToThinnedProcess($process);
495    }
496
497    public static function getProcessById(?int $processId, ?string $authKey, ?AuthenticatedUser $user): Process
498    {
499        // AuthKey check needs to be first
500        if (!is_null($authKey)) {
501            return ZmsApiClientService::getProcessById($processId, $authKey);
502        } elseif (!is_null($user)) {
503            $externalUserId = $user->getExternalUserId();
504            return ZmsApiClientService::getProcessByIdAuthenticated($processId, $externalUserId);
505        } else {
506            throw new UnauthorizedException();
507        }
508    }
509
510    /**
511     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
512     * @SuppressWarnings(PHPMD.NPathComplexity)
513     */
514    public static function getThinnedProcessById(int $processId, ?string $authKey, ?AuthenticatedUser $user): ThinnedProcess|array
515    {
516        $process = self::getProcessById($processId, $authKey, $user);
517        $errors = ValidationService::validateGetProcessNotFound($process);
518        if (is_array($errors) && !empty($errors['errors'])) {
519            return $errors;
520        }
521        return MapperService::processToThinnedProcess($process);
522    }
523
524    public static function updateClientData(Process $reservedProcess): Process|array
525    {
526        $clientUpdateResult = ZmsApiClientService::submitClientData($reservedProcess);
527        if (isset($clientUpdateResult['error'])) {
528            return $clientUpdateResult;
529        }
530        return $clientUpdateResult;
531    }
532
533    public static function preconfirmAppointment(Process $reservedProcess): Process|array
534    {
535        $clientUpdateResult = ZmsApiClientService::preconfirmProcess($reservedProcess);
536        if (isset($clientUpdateResult['error'])) {
537            return $clientUpdateResult;
538        }
539        return $clientUpdateResult;
540    }
541
542    public static function confirmAppointment(Process $preconfirmedProcess): Process|array
543    {
544        $clientUpdateResult = ZmsApiClientService::confirmProcess($preconfirmedProcess);
545        if (isset($clientUpdateResult['error'])) {
546            return $clientUpdateResult;
547        }
548        return $clientUpdateResult;
549    }
550
551    public static function cancelAppointment(Process $confirmedProcess): Process|array
552    {
553        $clientUpdateResult = ZmsApiClientService::cancelAppointment($confirmedProcess);
554        if (isset($clientUpdateResult['error'])) {
555            return $clientUpdateResult;
556        }
557        return $clientUpdateResult;
558    }
559
560    public static function sendPreconfirmationEmail(Process $reservedProcess): Process|array
561    {
562        $clientUpdateResult = ZmsApiClientService::sendPreconfirmationEmail($reservedProcess);
563        if (isset($clientUpdateResult['error'])) {
564            return $clientUpdateResult;
565        }
566        return $clientUpdateResult;
567    }
568
569    public static function sendConfirmationEmail(Process $preconfirmedProcess): Process|array
570    {
571        $clientUpdateResult = ZmsApiClientService::sendConfirmationEmail($preconfirmedProcess);
572        if (isset($clientUpdateResult['error'])) {
573            return $clientUpdateResult;
574        }
575        return $clientUpdateResult;
576    }
577
578    public static function sendCancellationEmail(Process $confirmedProcess): Process|array
579    {
580        $clientUpdateResult = ZmsApiClientService::sendCancellationEmail($confirmedProcess);
581        if (isset($clientUpdateResult['error'])) {
582            return $clientUpdateResult;
583        }
584        return $clientUpdateResult;
585    }
586
587    public static function getAppointmentsByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null): ProcessList
588    {
589        return ZmsApiClientService::getProcessesByExternalUserId($externalUserId, $filterId, $status);
590    }
591}