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