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