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