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