Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.13% covered (success)
90.13%
502 / 557
15.38% covered (danger)
15.38%
4 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZmsApiFacadeService
90.13% covered (success)
90.13%
502 / 557
15.38% covered (danger)
15.38%
4 / 26
223.39
0.00% covered (danger)
0.00%
0 / 1
 setLanguageContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
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
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
8.02
 getServicesAndOffices
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 getScopeByOfficeId
95.89% covered (success)
95.89%
70 / 73
0.00% covered (danger)
0.00%
0 / 1
14
 getOfficeListByServiceId
92.50% covered (success)
92.50%
37 / 40
0.00% covered (danger)
0.00%
0 / 1
16.11
 getScopeById
97.83% covered (success)
97.83%
45 / 46
0.00% covered (danger)
0.00%
0 / 1
14
 getServicesByOfficeId
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
13
 getBookableFreeDays
92.73% covered (success)
92.73%
51 / 55
0.00% covered (danger)
0.00%
0 / 1
13.07
 getFreeAppointments
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
7.06
 processFreeSlots
51.52% covered (warning)
51.52%
17 / 33
0.00% covered (danger)
0.00%
0 / 1
65.59
 getAvailableAppointments
91.67% covered (success)
91.67%
33 / 36
0.00% covered (danger)
0.00%
0 / 1
11.07
 reserveTimeslot
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getProcessById
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
11.53
 getThinnedProcessById
97.62% covered (success)
97.62%
41 / 42
0.00% covered (danger)
0.00%
0 / 1
12
 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\Zmscitizenapi\Exceptions\UnauthorizedException;
8use BO\Zmscitizenapi\Models\AuthenticatedUser;
9use BO\Zmscitizenapi\Utils\DateTimeFormatHelper;
10use BO\Zmscitizenapi\Utils\ErrorMessages;
11use BO\Zmscitizenapi\Models\AvailableAppointmentsByOffice;
12use BO\Zmscitizenapi\Models\AvailableDays;
13use BO\Zmscitizenapi\Models\AvailableAppointments;
14use BO\Zmscitizenapi\Models\AvailableDaysByOffice;
15use BO\Zmscitizenapi\Models\Office;
16use BO\Zmscitizenapi\Models\Service;
17use BO\Zmscitizenapi\Models\ThinnedProcess;
18use BO\Zmscitizenapi\Models\ThinnedScope;
19use BO\Zmscitizenapi\Models\Collections\OfficeList;
20use BO\Zmscitizenapi\Models\Collections\OfficeServiceRelationList;
21use BO\Zmscitizenapi\Models\Collections\OfficeServiceAndRelationList;
22use BO\Zmscitizenapi\Models\Collections\ServiceList;
23use BO\Zmscitizenapi\Models\Collections\ThinnedScopeList;
24use BO\Zmsentities\Calendar;
25use BO\Zmsentities\Collection\RequestRelationList;
26use BO\Zmsentities\Process;
27use BO\Zmsentities\Scope;
28use BO\Zmsentities\Collection\ScopeList;
29use BO\Zmsentities\Collection\ProviderList;
30use BO\Zmsentities\Collection\RequestList;
31use BO\Zmsentities\Collection\ProcessList;
32
33/**
34 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
35 * @TODO: Break down this facade into smaller domain-specific facades or use the Command pattern
36 */
37class ZmsApiFacadeService
38{
39    private const CACHE_KEY_OFFICES = 'processed_offices';
40    private const CACHE_KEY_SCOPES = 'processed_scopes';
41    private const CACHE_KEY_SERVICES = 'processed_services';
42    private const CACHE_KEY_OFFICES_AND_SERVICES = 'processed_offices_and_services';
43    private const CACHE_KEY_OFFICES_BY_SERVICE_PREFIX = 'processed_offices_by_service_';
44    private const CACHE_KEY_SERVICES_BY_OFFICE_PREFIX = 'processed_services_by_office_';
45
46    private static ?string $currentLanguage = null;
47    public static function setLanguageContext(?string $language): void
48    {
49        self::$currentLanguage = $language;
50    }
51
52    private static function getError(string $key): array
53    {
54        return ErrorMessages::get($key, self::$currentLanguage);
55    }
56
57    private static function setMappedCache(string $cacheKey, mixed $data): void
58    {
59        if (\App::$cache) {
60            \App::$cache->set($cacheKey, $data, \App::$SOURCE_CACHE_TTL);
61            LoggerService::logInfo('Second-level cache set', [
62                'key' => $cacheKey,
63                'ttl' => \App::$SOURCE_CACHE_TTL
64            ]);
65        }
66    }
67
68    /**
69     * @SuppressWarnings(PHPMD.NPathComplexity)
70     */
71    public static function getOffices(bool $showUnpublished = false): OfficeList
72    {
73        $cacheKey = self::CACHE_KEY_OFFICES . ($showUnpublished ? '_unpublished' : '');
74
75        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
76            return $cachedData;
77        }
78
79        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
80        $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
81        $offices = [];
82        $scopeMap = [];
83        foreach ($scopeList as $scope) {
84            if ($scope->getProvider()) {
85                $scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
86            }
87        }
88
89        foreach ($providerList as $provider) {
90            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
91                continue;
92            }
93
94            $matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
95            $offices[] = new Office(
96                id: (int) $provider->id,
97                name: $provider->displayName ?? $provider->name,
98                address: $provider->data['address'] ?? null,
99                showAlternativeLocations: $provider->data['showAlternativeLocations'] ?? null,
100                displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
101                organization: $provider->data['organization'] ?? null,
102                organizationUnit: $provider->data['organizationUnit'] ?? null,
103                slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
104                geo: $provider->data['geo'] ?? null,
105                scope: $matchingScope ? new ThinnedScope(
106                    id: (int) $matchingScope->id,
107                    provider: MapperService::providerToThinnedProvider($provider),
108                    shortName: (string) $matchingScope->getShortName(),
109                    emailFrom: (string) $matchingScope->getEmailFrom(),
110                    emailRequired: (bool) $matchingScope->getEmailRequired(),
111                    telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
112                    telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
113                    customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
114                    customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
115                    customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
116                    customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated(),
117                    customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required(),
118                    customTextfield2Label: $matchingScope->getCustomTextfield2Label(),
119                    captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
120                    infoForAppointment: $matchingScope->getInfoForAppointment(),
121                    infoForAllAppointments: $matchingScope->getInfoForAllAppointments(),
122                    slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()),
123                    appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()),
124                    whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()),
125                    activationDuration: MapperService::extractActivationDuration($matchingScope),
126                    reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
127                    hint: ($matchingScope && trim((string) $matchingScope->getScopeHint()) !== '')  ? (string) $matchingScope->getScopeHint() : null
128                ) : null,
129                maxSlotsPerAppointment: $matchingScope ? ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) : null
130            );
131        }
132
133        $result = new OfficeList($offices);
134
135        self::setMappedCache($cacheKey, $result);
136
137        return $result;
138    }
139
140    /**
141     * @SuppressWarnings(PHPMD.NPathComplexity)
142     */
143    public static function getScopes(): ThinnedScopeList|array
144    {
145        $cacheKey = self::CACHE_KEY_SCOPES;
146
147        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
148            return $cachedData;
149        }
150
151        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
152        $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
153        $scopeMap = [];
154        foreach ($scopeList as $scope) {
155            $scopeProvider = $scope->getProvider();
156            if ($scopeProvider && $scopeProvider->id && $scopeProvider->source) {
157                $key = $scopeProvider->source . '_' . $scopeProvider->id;
158                $scopeMap[$key] = $scope;
159            }
160        }
161
162        $scopesProjectionList = [];
163        foreach ($providerList as $provider) {
164            $key = $provider->source . '_' . $provider->id;
165            if (isset($scopeMap[$key])) {
166                $matchingScope = $scopeMap[$key];
167                $scopesProjectionList[] = new ThinnedScope(
168                    id: (int) $matchingScope->id,
169                    provider: MapperService::providerToThinnedProvider($provider),
170                    shortName: (string) $matchingScope->getShortName(),
171                    emailFrom: (string) $matchingScope->getEmailFrom(),
172                    emailRequired: (bool) $matchingScope->getEmailRequired(),
173                    telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
174                    telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
175                    customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
176                    customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
177                    customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
178                    customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated(),
179                    customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required(),
180                    customTextfield2Label: $matchingScope->getCustomTextfield2Label(),
181                    captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
182                    infoForAppointment: $matchingScope->getInfoForAppointment(),
183                    infoForAllAppointments: $matchingScope->getInfoForAllAppointments(),
184                    slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()),
185                    appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()),
186                    whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()),
187                    reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
188                    activationDuration: MapperService::extractActivationDuration($matchingScope),
189                    hint: ($matchingScope && trim((string) $matchingScope->getScopeHint()) !== '') ? (string) $matchingScope->getScopeHint() : null
190                );
191            }
192        }
193
194        $result = new ThinnedScopeList($scopesProjectionList);
195
196        self::setMappedCache($cacheKey, $result);
197
198        return $result;
199    }
200
201    public static function getServices(bool $showUnpublished = false): ServiceList|array
202    {
203        $cacheKey = self::CACHE_KEY_SERVICES . ($showUnpublished ? '_unpublished' : '');
204
205        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
206            return $cachedData;
207        }
208
209        $requestList = ZmsApiClientService::getServices() ?? new RequestList();
210        $services = [];
211        foreach ($requestList as $request) {
212            $additionalData = $request->getAdditionalData();
213            if (
214                !$showUnpublished
215                && isset($additionalData['public'])
216                && !$additionalData['public']
217            ) {
218                continue;
219            }
220
221            $services[] = new Service(id: (int) $request->getId(), name: $request->getName(), maxQuantity: $additionalData['maxQuantity'] ?? 1);
222        }
223
224        $result = new ServiceList($services);
225
226        self::setMappedCache($cacheKey, $result);
227
228        return $result;
229    }
230
231    public static function getServicesAndOffices(bool $showUnpublished = false): OfficeServiceAndRelationList|array
232    {
233        $cacheKey = self::CACHE_KEY_OFFICES_AND_SERVICES . ($showUnpublished ? '_unpublished' : '');
234
235        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
236            return $cachedData;
237        }
238
239        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
240        $requestList = ZmsApiClientService::getServices() ?? new RequestList();
241        $relationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
242
243        $offices = MapperService::mapOfficesWithScope($providerList, $showUnpublished) ?? new OfficeList();
244        $services = MapperService::mapServicesWithCombinations(
245            $requestList,
246            $relationList,
247            $showUnpublished
248        ) ?? new ServiceList();
249        $relations = MapperService::mapRelations($relationList, $showUnpublished) ?? new OfficeServiceRelationList();
250
251        $result = new OfficeServiceAndRelationList($offices, $services, $relations);
252
253        self::setMappedCache($cacheKey, $result);
254
255        return $result;
256    }
257
258    /* Todo add method
259     * getCombinableServicesByIds
260     *
261     *
262     *
263     */
264
265    /**
266     * @SuppressWarnings(PHPMD.NPathComplexity)
267     */
268    public static function getScopeByOfficeId(int $officeId): ThinnedScope|array
269    {
270        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
271        $selectedProvider = null;
272        foreach ($providerList as $provider) {
273            if ((int) $provider->id === (int) $officeId) {
274                $selectedProvider = $provider;
275                break;
276            }
277        }
278        if (!$selectedProvider) {
279            return ['errors' => [self::getError('officeNotFound')]];
280        }
281
282        $scopeSource = (string) ($selectedProvider->source ?? '');
283        if ($scopeSource === '') {
284            return ['errors' => [self::getError('scopeNotFound')]];
285        }
286
287        $matchingScope = ZmsApiClientService::getScopesByProviderId($scopeSource, (int) $officeId)
288            ->getIterator()
289            ->current();
290
291        if (!$matchingScope instanceof Scope) {
292            return ['errors' => [self::getError('scopeNotFound')]];
293        }
294
295        $providerMap = [];
296        foreach ($providerList as $prov) {
297            $key = ($prov->source ?? '') . '_' . $prov->id;
298            $providerMap[$key] = $prov;
299        }
300
301        $scopeProvider = $matchingScope->getProvider();
302        $providerKey = $scopeProvider ? (($scopeProvider->source ?? '') . '_' . $scopeProvider->id) : null;
303        $finalProvider = ($providerKey && isset($providerMap[$providerKey]))
304            ? $providerMap[$providerKey]
305            : $scopeProvider;
306        $result = [
307            'id' => $matchingScope->id,
308            'provider' => MapperService::providerToThinnedProvider($finalProvider) ?? null,
309            'shortName' => (string) $matchingScope->getShortName() ?? null,
310            'emailFrom' => (string) $matchingScope->getEmailFrom() ?? null,
311            'emailRequired' => (bool) $matchingScope->getEmailRequired() ?? null,
312            'telephoneActivated' => (bool) $matchingScope->getTelephoneActivated() ?? null,
313            'telephoneRequired' => (bool) $matchingScope->getTelephoneRequired() ?? null,
314            'customTextfieldActivated' => (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
315            'customTextfieldRequired' => (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
316            'customTextfieldLabel' => $matchingScope->getCustomTextfieldLabel() ?? null,
317            'customTextfield2Activated' => (bool) $matchingScope->getCustomTextfield2Activated() ?? null,
318            'customTextfield2Required' => (bool) $matchingScope->getCustomTextfield2Required() ?? null,
319            'customTextfield2Label' => $matchingScope->getCustomTextfield2Label() ?? null,
320            'captchaActivatedRequired' => (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
321            'infoForAppointment' => $matchingScope->getInfoForAppointment() ?? null,
322            'infoForAllAppointments' => $matchingScope->getInfoForAllAppointments() ?? null,
323            'slotsPerAppointment' => ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) ?? null,
324            'appointmentsPerMail' => ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()) ?? null,
325            'whitelistedMails' => ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()) ?? null,
326            'reservationDuration' => (int) MapperService::extractReservationDuration($matchingScope),
327            'activationDuration' => MapperService::extractActivationDuration($matchingScope),
328            'hint' => (trim((string) ($matchingScope->getScopeHint() ?? '')) === '') ? null : (string) $matchingScope->getScopeHint()
329        ];
330        return new ThinnedScope(
331            id: (int) $result['id'],
332            provider: $result['provider'],
333            shortName: $result['shortName'],
334            emailFrom: $result['emailFrom'],
335            emailRequired: $result['emailRequired'],
336            telephoneActivated: $result['telephoneActivated'],
337            telephoneRequired: $result['telephoneRequired'],
338            customTextfieldActivated: $result['customTextfieldActivated'],
339            customTextfieldRequired: $result['customTextfieldRequired'],
340            customTextfieldLabel: $result['customTextfieldLabel'],
341            customTextfield2Activated: $result['customTextfield2Activated'],
342            customTextfield2Required: $result['customTextfield2Required'],
343            customTextfield2Label: $result['customTextfield2Label'],
344            captchaActivatedRequired: $result['captchaActivatedRequired'],
345            infoForAppointment: $result['infoForAppointment'],
346            infoForAllAppointments: $result['infoForAllAppointments'],
347            slotsPerAppointment: $result['slotsPerAppointment'],
348            appointmentsPerMail: $result['appointmentsPerMail'],
349            whitelistedMails: $result['whitelistedMails'],
350            reservationDuration: $result['reservationDuration'],
351            activationDuration: $result['activationDuration'],
352            hint: $result['hint']
353        );
354    }
355
356    /* Todo add method
357     * getOfficeById
358     *
359     *
360     *
361     */
362
363    /**
364     * @SuppressWarnings(PHPMD.NPathComplexity)
365     * @TODO: Extract providerMap mapping logic into MapperService
366     */
367    public static function getOfficeListByServiceId(int $serviceId, bool $showUnpublished = false): OfficeList|array
368    {
369        $cacheKey = self::CACHE_KEY_OFFICES_BY_SERVICE_PREFIX . $serviceId . ($showUnpublished ? '_unpublished' : '');
370
371        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
372            return $cachedData;
373        }
374
375        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
376        $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
377        $providerMap = [];
378        foreach ($providerList as $provider) {
379            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
380                continue;
381            }
382
383            $providerMap[$provider->id] = $provider;
384        }
385
386        $offices = [];
387        foreach ($requestRelationList as $relation) {
388            if ((int) $relation->request->id === $serviceId) {
389                $providerId = $relation->provider->id;
390                if (!isset($providerMap[$providerId])) {
391                    continue;
392                }
393
394                $provider = $providerMap[$providerId];
395                $scope = null;
396                $scopeData = self::getScopeByOfficeId((int) $provider->id);
397                if ($scopeData instanceof ThinnedScope) {
398                    $scope = $scopeData;
399                }
400
401                $offices[] = new Office(
402                    id: (int) $provider->id,
403                    name: $provider->name,
404                    showAlternativeLocations: $provider->data['showAlternativeLocations'] ?? null,
405                    displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
406                    organization: $provider->data['organization'] ?? null,
407                    organizationUnit: $provider->data['organizationUnit'] ?? null,
408                    slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
409                    address: $provider->address ?? null,
410                    geo: $provider->geo ?? null,
411                    scope: $scope,
412                    maxSlotsPerAppointment: $scope ? ((string) $scope->getSlotsPerAppointment() === '' ? null : (string) $scope->getSlotsPerAppointment()) : null
413                );
414            }
415        }
416
417        $errors = ValidationService::validateOfficesNotFound($offices);
418        if (is_array($errors) && !empty($errors['errors'])) {
419            return $errors;
420        }
421
422        $result = new OfficeList($offices);
423
424        self::setMappedCache($cacheKey, $result);
425
426        return $result;
427    }
428
429    /**
430     * @SuppressWarnings(PHPMD.NPathComplexity)
431     */
432    public static function getScopeById(?int $scopeId): ThinnedScope|array
433    {
434        $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
435        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
436        $matchingScope = null;
437        foreach ($scopeList as $scope) {
438            if ((int) $scope->id === (int) $scopeId) {
439                $matchingScope = $scope;
440                break;
441            }
442        }
443
444        $tempScopeList = new ScopeList();
445        if ($matchingScope !== null) {
446            $tempScopeList->addEntity($matchingScope);
447        }
448        $errors = ValidationService::validateScopesNotFound($tempScopeList);
449        if (is_array($errors) && !empty($errors['errors'])) {
450            return $errors;
451        }
452
453        $providerMap = [];
454        foreach ($providerList as $provider) {
455            $key = $provider->source . '_' . $provider->id;
456            $providerMap[$key] = $provider;
457        }
458
459        $scopeProvider = $matchingScope->getProvider();
460        $providerKey = $scopeProvider ? ($scopeProvider->source . '_' . $scopeProvider->id) : null;
461        $matchingProv = ($providerKey && isset($providerMap[$providerKey]))
462            ? $providerMap[$providerKey]
463            : $scopeProvider;
464        return new ThinnedScope(
465            id: (int) $matchingScope->id,
466            provider: MapperService::providerToThinnedProvider($matchingProv),
467            shortName: (string) $matchingScope->getShortName() ?? null,
468            emailFrom: (string) $matchingScope->getEmailFrom() ?? null,
469            emailRequired: (bool) $matchingScope->getEmailRequired() ?? null,
470            telephoneActivated: (bool) $matchingScope->getTelephoneActivated() ?? null,
471            telephoneRequired: (bool) $matchingScope->getTelephoneRequired() ?? null,
472            customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
473            customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
474            customTextfieldLabel: $matchingScope->getCustomTextfieldLabel() ?? null,
475            customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated() ?? null,
476            customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required() ?? null,
477            customTextfield2Label: $matchingScope->getCustomTextfield2Label() ?? null,
478            captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
479            infoForAppointment: $matchingScope->getInfoForAppointment() ?? null,
480            infoForAllAppointments: $matchingScope->getInfoForAllAppointments() ?? null,
481            slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) ?? null,
482            appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()) ?? null,
483            whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()) ?? null,
484            reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
485            activationDuration: MapperService::extractActivationDuration($matchingScope),
486            hint: ((string) $matchingScope->getScopeHint() === '' ? null : (string) $matchingScope->getScopeHint()) ?? null
487        );
488    }
489
490    public static function getServicesByOfficeId(int $officeId, bool $showUnpublished = false): ServiceList|array
491    {
492        $cacheKey = self::CACHE_KEY_SERVICES_BY_OFFICE_PREFIX . $officeId . ($showUnpublished ? '_unpublished' : '');
493
494        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
495            return $cachedData;
496        }
497
498        $requestList = ZmsApiClientService::getServices() ?? new RequestList();
499        $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
500        $requestMap = [];
501        foreach ($requestList as $request) {
502            $additionalData = $request->getAdditionalData();
503            if (
504                !$showUnpublished
505                && isset($additionalData['public'])
506                && !$additionalData['public']
507            ) {
508                continue;
509            }
510
511            $requestMap[$request->id] = $request;
512        }
513
514        $services = [];
515        foreach ($requestRelationList as $relation) {
516            if ((int) $relation->provider->id === $officeId) {
517                $requestId = $relation->request->id;
518                if (isset($requestMap[$requestId])) {
519                    $request = $requestMap[$requestId];
520                    $services[] = new Service(id: (int) $request->id, name: $request->name, maxQuantity: $request->getAdditionalData()['maxQuantity'] ?? 1);
521                }
522            }
523        }
524
525        $errors = ValidationService::validateServicesNotFound($services);
526        if (is_array($errors) && !empty($errors['errors'])) {
527            return $errors;
528        }
529
530        $result = new ServiceList($services);
531
532        self::setMappedCache($cacheKey, $result);
533
534        return $result;
535    }
536
537    /**
538     * @SuppressWarnings(PHPMD.NPathComplexity)
539     */
540    public static function getBookableFreeDays(
541        array $officeIds,
542        array $serviceIds,
543        array $serviceCounts,
544        string $startDate,
545        string $endDate,
546        ?bool $groupByOffice = false
547    ): AvailableDays|AvailableDaysByOffice|array {
548        $firstDay = DateTimeFormatHelper::getInternalDateFromISO($startDate);
549        $lastDay  = DateTimeFormatHelper::getInternalDateFromISO($endDate);
550
551        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
552        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
553
554        $providerSource = [];
555        foreach ($providerList as $p) {
556            $providerSource[(string)$p->id] = (string)($p->source ?? '');
557        }
558
559        $requestSource = [];
560        foreach ($requestList as $r) {
561            $requestSource[(string)$r->id] = (string)($r->source ?? '');
562        }
563
564        $services = [];
565        foreach ($serviceIds as $i => $serviceId) {
566            $sid = (string)$serviceId;
567            $src = $requestSource[$sid] ?? null;
568            if (!$src) {
569                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
570            }
571            $services[] = [
572                'id'        => $serviceId,
573                'source'    => $src,
574                'slotCount' => (int)($serviceCounts[$i] ?? 1),
575            ];
576        }
577
578        $providers = [];
579        foreach ($officeIds as $officeId) {
580            $oid = (string)$officeId;
581            $src = $providerSource[$oid] ?? null;
582            if (!$src) {
583                return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
584            }
585            $providers[] = [
586                'id'     => $officeId,
587                'source' => $src,
588            ];
589        }
590
591        $freeDays = ZmsApiClientService::getFreeDays(
592            new ProviderList($providers),
593            new RequestList($services),
594            $firstDay,
595            $lastDay
596        ) ?? new Calendar();
597
598        $daysCollection  = $freeDays->days;
599        $formattedDays   = [];
600        $scopeToProvider = [];
601
602        foreach ($freeDays->scopes as $scope) {
603            $scopeToProvider[$scope['id']] = $scope['provider']['id'];
604        }
605
606        foreach ($daysCollection as $day) {
607            $formattedDays[] = [
608                'time'        => sprintf('%04d-%02d-%02d', $day->year, $day->month, $day->day),
609                'providerIDs' => isset($day->scopeIDs)
610                    ? implode(',', array_map(fn($scopeId) => $scopeToProvider[$scopeId], explode(',', $day->scopeIDs)))
611                    : ''
612            ];
613        }
614
615        $errors = ValidationService::validateAppointmentDaysNotFound($formattedDays);
616        if (is_array($errors) && !empty($errors['errors'])) {
617            return $errors;
618        }
619
620        return $groupByOffice
621            ? new AvailableDaysByOffice($formattedDays)
622            : new AvailableDays(array_column($formattedDays, 'time'));
623    }
624
625    public static function getFreeAppointments(int $officeId, array $serviceIds, array $serviceCounts, array $date): ProcessList|array
626    {
627        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
628        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
629
630        $providerSource = [];
631        foreach ($providerList as $p) {
632            $providerSource[(string)$p->id] = (string)($p->source ?? '');
633        }
634        $requestSource  = [];
635        foreach ($requestList as $r) {
636            $requestSource[(string)$r->id] = (string)($r->source ?? '');
637        }
638
639        $oid = (string)$officeId;
640        $provSrc = $providerSource[$oid] ?? null;
641        if (!$provSrc) {
642            return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
643        }
644
645        $office = ['id' => $officeId, 'source' => $provSrc];
646
647        $requests = [];
648        foreach ($serviceIds as $id => $serviceId) {
649            $sid   = (string)$serviceId;
650            $reqSrc = $requestSource[$sid] ?? null;
651            if (!$reqSrc) {
652                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
653            }
654            $count = (int)($serviceCounts[$id] ?? 1);
655            for ($k = 0; $k < $count; $k++) {
656                $requests[] = ['id' => $serviceId, 'source' => $reqSrc];
657            }
658        }
659
660        return ZmsApiClientService::getFreeTimeslots(
661            new ProviderList([$office]),
662            new RequestList($requests),
663            $date,
664            $date
665        );
666    }
667
668    /**
669     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
670     */
671    private static function processFreeSlots(ProcessList $freeSlots, bool $groupByOffice = false): array
672    {
673        $errors = ValidationService::validateGetProcessFreeSlots($freeSlots);
674        if (is_array($errors) && !empty($errors['errors'])) {
675            return $errors;
676        }
677
678        $currentTimestamp = time();
679        if ($groupByOffice) {
680            $grouped = [];
681            foreach ($freeSlots as $slot) {
682                $officeId = (string)($slot->scope->provider->id ?? '');
683                if (!isset($grouped[$officeId])) {
684                    $grouped[$officeId] = [];
685                }
686                if (isset($slot->appointments) && is_iterable($slot->appointments)) {
687                    foreach ($slot->appointments as $appointment) {
688                        if (isset($appointment->date)) {
689                            $timestamp = (int) $appointment->date;
690                            if ($timestamp > $currentTimestamp) {
691                                $grouped[$officeId][] = $timestamp;
692                            }
693                        }
694                    }
695                }
696            }
697            // Sort each office's appointments
698            foreach ($grouped as &$arr) {
699                sort($arr);
700            }
701            unset($arr);
702            // Optionally validate grouped timestamps here if needed
703            return $grouped;
704        } else {
705            $timestamps = [];
706            foreach ($freeSlots as $slot) {
707                if (isset($slot->appointments) && is_iterable($slot->appointments)) {
708                    foreach ($slot->appointments as $appointment) {
709                        if (isset($appointment->date)) {
710                            $timestamp = (int) $appointment->date;
711                            if ($timestamp > $currentTimestamp) {
712                                $timestamps[] = $timestamp;
713                            }
714                        }
715                    }
716                }
717            }
718            sort($timestamps);
719
720            $errors = ValidationService::validateGetProcessByIdTimestamps($timestamps);
721            if (is_array($errors) && !empty($errors['errors'])) {
722                return $errors;
723            }
724
725            return $timestamps;
726        }
727    }
728
729    public static function getAvailableAppointments(
730        string $date,
731        array $officeIds,
732        array $serviceIds,
733        array $serviceCounts,
734        ?bool $groupByOffice = false
735    ): AvailableAppointments|AvailableAppointmentsByOffice|array {
736        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
737        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
738
739        $providerSource = [];
740        foreach ($providerList as $p) {
741            $providerSource[(string)$p->id] = (string)($p->source ?? '');
742        }
743        $requestSource  = [];
744        foreach ($requestList as $r) {
745            $requestSource[(string)$r->id] = (string)($r->source ?? '');
746        }
747
748        $requests = [];
749        foreach ($serviceIds as $id => $serviceId) {
750            $sid = (string)$serviceId;
751            $src = $requestSource[$sid] ?? null;
752            if (!$src) {
753                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
754            }
755            $count = (int)($serviceCounts[$id] ?? 1);
756            for ($k = 0; $k < $count; $k++) {
757                $requests[] = ['id' => $serviceId, 'source' => $src];
758            }
759        }
760
761        $providers = [];
762        foreach ($officeIds as $officeId) {
763            $oid = (string)$officeId;
764            $src = $providerSource[$oid] ?? null;
765            if (!$src) {
766                return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
767            }
768            $providers[] = ['id' => $officeId, 'source' => $src];
769        }
770
771        $freeSlots = ZmsApiClientService::getFreeTimeslots(
772            new ProviderList($providers),
773            new RequestList($requests),
774            DateTimeFormatHelper::getInternalDateFromISO($date),
775            DateTimeFormatHelper::getInternalDateFromISO($date)
776        ) ?? new ProcessList();
777
778        $result = self::processFreeSlots($freeSlots, $groupByOffice);
779        if (isset($result['errors']) && !empty($result['errors'])) {
780            return $result;
781        }
782
783        return $groupByOffice
784            ? new AvailableAppointmentsByOffice($result)
785            : new AvailableAppointments($result);
786    }
787
788    public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): ThinnedProcess|array
789    {
790        $errors = ValidationService::validateServiceArrays($serviceIds, $serviceCounts);
791        if (!empty($errors)) {
792            return $errors;
793        }
794        $process = ZmsApiClientService::reserveTimeslot($appointmentProcess, $serviceIds, $serviceCounts);
795        return MapperService::processToThinnedProcess($process);
796    }
797
798    public static function getProcessById(?int $processId, ?string $authKey, ?AuthenticatedUser $user): Process
799    {
800        // AuthKey check needs to be first
801        if (!is_null($authKey)) {
802            return ZmsApiClientService::getProcessById($processId, $authKey);
803        } elseif (!is_null($user)) {
804            $externalUserId = $user->getExternalUserId();
805            $process = ZmsApiClientService::getProcessByIdAuthenticated($processId);
806            if ($externalUserId !== $process->getExternalUserId()) {
807                throw new UnauthorizedException();
808            }
809            return $process;
810        } else {
811            throw new UnauthorizedException();
812        }
813    }
814
815    /**
816     * @SuppressWarnings(PHPMD.NPathComplexity)
817     */
818    public static function getThinnedProcessById(int $processId, ?string $authKey, ?AuthenticatedUser $user): ThinnedProcess|array
819    {
820        $process = self::getProcessById($processId, $authKey, $user);
821        $errors = ValidationService::validateGetProcessNotFound($process);
822        if (is_array($errors) && !empty($errors['errors'])) {
823            return $errors;
824        }
825        $thinnedProcess = MapperService::processToThinnedProcess($process);
826
827        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
828        $providerMap = [];
829        foreach ($providerList as $provider) {
830            $key = $provider->getSource() . '_' . $provider->id;
831            $providerMap[$key] = $provider;
832        }
833
834        $thinnedScope = null;
835        if ($process->scope instanceof Scope) {
836            $scopeProvider = $process->scope->getProvider();
837            $providerKey = $scopeProvider ? ($scopeProvider->getSource() . '_' . $scopeProvider->id) : null;
838            $matchingProvider = $providerKey && isset($providerMap[$providerKey]) ? $providerMap[$providerKey] : $scopeProvider;
839            $thinnedProvider = MapperService::providerToThinnedProvider($matchingProvider);
840            $thinnedScope = new ThinnedScope(
841                id: (int) $process->scope->id,
842                provider: $thinnedProvider,
843                shortName: (string) $process->scope->getShortName() ?? null,
844                emailFrom: (string) $process->scope->getEmailFrom() ?? null,
845                emailRequired: (bool) $process->scope->getEmailRequired() ?? false,
846                telephoneActivated: (bool) $process->scope->getTelephoneActivated() ?? false,
847                telephoneRequired: (bool) $process->scope->getTelephoneRequired() ?? false,
848                customTextfieldActivated: (bool) $process->scope->getCustomTextfieldActivated() ?? false,
849                customTextfieldRequired: (bool) $process->scope->getCustomTextfieldRequired() ?? false,
850                customTextfieldLabel: $process->scope->getCustomTextfieldLabel() ?? null,
851                customTextfield2Activated: (bool) $process->scope->getCustomTextfield2Activated() ?? false,
852                customTextfield2Required: (bool) $process->scope->getCustomTextfield2Required() ?? false,
853                customTextfield2Label: $process->scope->getCustomTextfield2Label() ?? null,
854                captchaActivatedRequired: (bool) $process->scope->getCaptchaActivatedRequired() ?? false,
855                infoForAppointment: $process->scope->getInfoForAppointment() ?? null,
856                infoForAllAppointments: $process->scope->getInfoForAllAppointments() ?? null,
857                slotsPerAppointment: ((string) $process->scope->getSlotsPerAppointment() === '' ? null : (string) $process->scope->getSlotsPerAppointment()) ?? null,
858                appointmentsPerMail: ((string) $process->scope->getAppointmentsPerMail() === '' ? null : (string) $process->scope->getAppointmentsPerMail()) ?? null,
859                whitelistedMails: ((string) $process->scope->getWhitelistedMails() === '' ? null : (string) $process->scope->getWhitelistedMails()) ?? null,
860                reservationDuration: (int) MapperService::extractReservationDuration($process->scope),
861                activationDuration: MapperService::extractActivationDuration($process->scope),
862                hint: ((string) $process->scope->getScopeHint() === '' ? null : (string) $process->scope->getScopeHint()) ?? null
863            );
864        }
865
866        $thinnedProcess->scope = $thinnedScope;
867        return $thinnedProcess;
868    }
869
870    public static function updateClientData(Process $reservedProcess): Process|array
871    {
872        $clientUpdateResult = ZmsApiClientService::submitClientData($reservedProcess);
873        if (isset($clientUpdateResult['error'])) {
874            return $clientUpdateResult;
875        }
876        return $clientUpdateResult;
877    }
878
879    public static function preconfirmAppointment(Process $reservedProcess): Process|array
880    {
881        $clientUpdateResult = ZmsApiClientService::preconfirmProcess($reservedProcess);
882        if (isset($clientUpdateResult['error'])) {
883            return $clientUpdateResult;
884        }
885        return $clientUpdateResult;
886    }
887
888    public static function confirmAppointment(Process $preconfirmedProcess): Process|array
889    {
890        $clientUpdateResult = ZmsApiClientService::confirmProcess($preconfirmedProcess);
891        if (isset($clientUpdateResult['error'])) {
892            return $clientUpdateResult;
893        }
894        return $clientUpdateResult;
895    }
896
897    public static function cancelAppointment(Process $confirmedProcess): Process|array
898    {
899        $clientUpdateResult = ZmsApiClientService::cancelAppointment($confirmedProcess);
900        if (isset($clientUpdateResult['error'])) {
901            return $clientUpdateResult;
902        }
903        return $clientUpdateResult;
904    }
905
906    public static function sendPreconfirmationEmail(Process $reservedProcess): Process|array
907    {
908        $clientUpdateResult = ZmsApiClientService::sendPreconfirmationEmail($reservedProcess);
909        if (isset($clientUpdateResult['error'])) {
910            return $clientUpdateResult;
911        }
912        return $clientUpdateResult;
913    }
914
915    public static function sendConfirmationEmail(Process $preconfirmedProcess): Process|array
916    {
917        $clientUpdateResult = ZmsApiClientService::sendConfirmationEmail($preconfirmedProcess);
918        if (isset($clientUpdateResult['error'])) {
919            return $clientUpdateResult;
920        }
921        return $clientUpdateResult;
922    }
923
924    public static function sendCancellationEmail(Process $confirmedProcess): Process|array
925    {
926        $clientUpdateResult = ZmsApiClientService::sendCancellationEmail($confirmedProcess);
927        if (isset($clientUpdateResult['error'])) {
928            return $clientUpdateResult;
929        }
930        return $clientUpdateResult;
931    }
932
933    public static function getAppointmentsByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null): ProcessList
934    {
935        return ZmsApiClientService::getProcessesByExternalUserId($externalUserId, $filterId, $status);
936    }
937}