Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.22% covered (success)
91.22%
499 / 547
12.50% covered (danger)
12.50%
3 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZmsApiFacadeService
91.22% covered (success)
91.22%
499 / 547
12.50% covered (danger)
12.50%
3 / 24
206.88
0.00% covered (danger)
0.00%
0 / 1
 setLanguageContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMappedCache
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getOffices
96.23% covered (success)
96.23%
51 / 53
0.00% covered (danger)
0.00%
0 / 1
18
 getScopes
97.67% covered (success)
97.67%
42 / 43
0.00% covered (danger)
0.00%
0 / 1
14
 getServices
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
8.02
 getServicesAndOffices
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 getScopeByOfficeId
95.89% covered (success)
95.89%
70 / 73
0.00% covered (danger)
0.00%
0 / 1
14
 getOfficeListByServiceId
92.50% covered (success)
92.50%
37 / 40
0.00% covered (danger)
0.00%
0 / 1
16.11
 getScopeById
97.83% covered (success)
97.83%
45 / 46
0.00% covered (danger)
0.00%
0 / 1
14
 getServicesByOfficeId
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
13
 getBookableFreeDays
92.73% covered (success)
92.73%
51 / 55
0.00% covered (danger)
0.00%
0 / 1
13.07
 getFreeAppointments
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
7.06
 processFreeSlots
51.52% covered (warning)
51.52%
17 / 33
0.00% covered (danger)
0.00%
0 / 1
65.59
 getAvailableAppointments
91.67% covered (success)
91.67%
33 / 36
0.00% covered (danger)
0.00%
0 / 1
11.07
 reserveTimeslot
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 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\Utils\DateTimeFormatHelper;
8use BO\Zmscitizenapi\Utils\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    /**
262     * @SuppressWarnings(PHPMD.NPathComplexity)
263     */
264    public static function getScopeByOfficeId(int $officeId): ThinnedScope|array
265    {
266        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
267        $selectedProvider = null;
268        foreach ($providerList as $provider) {
269            if ((int) $provider->id === (int) $officeId) {
270                $selectedProvider = $provider;
271                break;
272            }
273        }
274        if (!$selectedProvider) {
275            return ['errors' => [self::getError('officeNotFound')]];
276        }
277
278        $scopeSource = (string) ($selectedProvider->source ?? '');
279        if ($scopeSource === '') {
280            return ['errors' => [self::getError('scopeNotFound')]];
281        }
282
283        $matchingScope = ZmsApiClientService::getScopesByProviderId($scopeSource, (int) $officeId)
284            ->getIterator()
285            ->current();
286
287        if (!$matchingScope instanceof Scope) {
288            return ['errors' => [self::getError('scopeNotFound')]];
289        }
290
291        $providerMap = [];
292        foreach ($providerList as $prov) {
293            $key = ($prov->source ?? '') . '_' . $prov->id;
294            $providerMap[$key] = $prov;
295        }
296
297        $scopeProvider = $matchingScope->getProvider();
298        $providerKey = $scopeProvider ? (($scopeProvider->source ?? '') . '_' . $scopeProvider->id) : null;
299        $finalProvider = ($providerKey && isset($providerMap[$providerKey]))
300            ? $providerMap[$providerKey]
301            : $scopeProvider;
302        $result = [
303            'id' => $matchingScope->id,
304            'provider' => MapperService::providerToThinnedProvider($finalProvider) ?? null,
305            'shortName' => (string) $matchingScope->getShortName() ?? null,
306            'emailFrom' => (string) $matchingScope->getEmailFrom() ?? null,
307            'emailRequired' => (bool) $matchingScope->getEmailRequired() ?? null,
308            'telephoneActivated' => (bool) $matchingScope->getTelephoneActivated() ?? null,
309            'telephoneRequired' => (bool) $matchingScope->getTelephoneRequired() ?? null,
310            'customTextfieldActivated' => (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
311            'customTextfieldRequired' => (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
312            'customTextfieldLabel' => $matchingScope->getCustomTextfieldLabel() ?? null,
313            'customTextfield2Activated' => (bool) $matchingScope->getCustomTextfield2Activated() ?? null,
314            'customTextfield2Required' => (bool) $matchingScope->getCustomTextfield2Required() ?? null,
315            'customTextfield2Label' => $matchingScope->getCustomTextfield2Label() ?? null,
316            'captchaActivatedRequired' => (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
317            'infoForAppointment' => $matchingScope->getInfoForAppointment() ?? null,
318            'infoForAllAppointments' => $matchingScope->getInfoForAllAppointments() ?? null,
319            'slotsPerAppointment' => ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) ?? null,
320            'appointmentsPerMail' => ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()) ?? null,
321            'whitelistedMails' => ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()) ?? null,
322            'reservationDuration' => (int) MapperService::extractReservationDuration($matchingScope),
323            'activationDuration' => MapperService::extractActivationDuration($matchingScope),
324            'hint' => (trim((string) ($matchingScope->getScopeHint() ?? '')) === '') ? null : (string) $matchingScope->getScopeHint(),
325        ];
326        return new ThinnedScope(
327            id: (int) $result['id'],
328            provider: $result['provider'],
329            shortName: $result['shortName'],
330            emailFrom: $result['emailFrom'],
331            emailRequired: $result['emailRequired'],
332            telephoneActivated: $result['telephoneActivated'],
333            telephoneRequired: $result['telephoneRequired'],
334            customTextfieldActivated: $result['customTextfieldActivated'],
335            customTextfieldRequired: $result['customTextfieldRequired'],
336            customTextfieldLabel: $result['customTextfieldLabel'],
337            customTextfield2Activated: $result['customTextfield2Activated'],
338            customTextfield2Required: $result['customTextfield2Required'],
339            customTextfield2Label: $result['customTextfield2Label'],
340            captchaActivatedRequired: $result['captchaActivatedRequired'],
341            infoForAppointment: $result['infoForAppointment'],
342            infoForAllAppointments: $result['infoForAllAppointments'],
343            slotsPerAppointment: $result['slotsPerAppointment'],
344            appointmentsPerMail: $result['appointmentsPerMail'],
345            whitelistedMails: $result['whitelistedMails'],
346            reservationDuration: $result['reservationDuration'],
347            activationDuration: $result['activationDuration'],
348            hint: $result['hint']
349        );
350    }
351
352    /* Todo add method
353     * getOfficeById
354     *
355     *
356     *
357     */
358
359    /**
360     * @SuppressWarnings(PHPMD.NPathComplexity)
361     * @TODO: Extract providerMap mapping logic into MapperService
362     */
363    public static function getOfficeListByServiceId(int $serviceId, bool $showUnpublished = false): OfficeList|array
364    {
365        $cacheKey = self::CACHE_KEY_OFFICES_BY_SERVICE_PREFIX . $serviceId . ($showUnpublished ? '_unpublished' : '');
366
367        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
368            return $cachedData;
369        }
370
371        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
372        $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
373        $providerMap = [];
374        foreach ($providerList as $provider) {
375            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
376                continue;
377            }
378
379            $providerMap[$provider->id] = $provider;
380        }
381
382        $offices = [];
383        foreach ($requestRelationList as $relation) {
384            if ((int) $relation->request->id === $serviceId) {
385                $providerId = $relation->provider->id;
386                if (!isset($providerMap[$providerId])) {
387                    continue;
388                }
389
390                $provider = $providerMap[$providerId];
391                $scope = null;
392                $scopeData = self::getScopeByOfficeId((int) $provider->id);
393                if ($scopeData instanceof ThinnedScope) {
394                    $scope = $scopeData;
395                }
396
397                $offices[] = new Office(
398                    id: (int) $provider->id,
399                    name: $provider->name,
400                    showAlternativeLocations: $provider->data['showAlternativeLocations'] ?? null,
401                    displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
402                    organization: $provider->data['organization'] ?? null,
403                    organizationUnit: $provider->data['organizationUnit'] ?? null,
404                    slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
405                    address: $provider->address ?? null,
406                    geo: $provider->geo ?? null,
407                    scope: $scope,
408                    maxSlotsPerAppointment: $scope ? ((string) $scope->getSlotsPerAppointment() === '' ? null : (string) $scope->getSlotsPerAppointment()) : null
409                );
410            }
411        }
412
413        $errors = ValidationService::validateOfficesNotFound($offices);
414        if (is_array($errors) && !empty($errors['errors'])) {
415            return $errors;
416        }
417
418        $result = new OfficeList($offices);
419
420        self::setMappedCache($cacheKey, $result);
421
422        return $result;
423    }
424
425    /**
426     * @SuppressWarnings(PHPMD.NPathComplexity)
427     */
428    public static function getScopeById(?int $scopeId): ThinnedScope|array
429    {
430        $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
431        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
432        $matchingScope = null;
433        foreach ($scopeList as $scope) {
434            if ((int) $scope->id === (int) $scopeId) {
435                $matchingScope = $scope;
436                break;
437            }
438        }
439
440        $tempScopeList = new ScopeList();
441        if ($matchingScope !== null) {
442            $tempScopeList->addEntity($matchingScope);
443        }
444        $errors = ValidationService::validateScopesNotFound($tempScopeList);
445        if (is_array($errors) && !empty($errors['errors'])) {
446            return $errors;
447        }
448
449        $providerMap = [];
450        foreach ($providerList as $provider) {
451            $key = $provider->source . '_' . $provider->id;
452            $providerMap[$key] = $provider;
453        }
454
455        $scopeProvider = $matchingScope->getProvider();
456        $providerKey = $scopeProvider ? ($scopeProvider->source . '_' . $scopeProvider->id) : null;
457        $matchingProv = ($providerKey && isset($providerMap[$providerKey]))
458            ? $providerMap[$providerKey]
459            : $scopeProvider;
460        return new ThinnedScope(
461            id: (int) $matchingScope->id,
462            provider: MapperService::providerToThinnedProvider($matchingProv),
463            shortName: (string) $matchingScope->getShortName() ?? null,
464            emailFrom: (string) $matchingScope->getEmailFrom() ?? null,
465            emailRequired: (bool) $matchingScope->getEmailRequired() ?? null,
466            telephoneActivated: (bool) $matchingScope->getTelephoneActivated() ?? null,
467            telephoneRequired: (bool) $matchingScope->getTelephoneRequired() ?? null,
468            customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
469            customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
470            customTextfieldLabel: $matchingScope->getCustomTextfieldLabel() ?? null,
471            customTextfield2Activated: (bool) $matchingScope->getCustomTextfield2Activated() ?? null,
472            customTextfield2Required: (bool) $matchingScope->getCustomTextfield2Required() ?? null,
473            customTextfield2Label: $matchingScope->getCustomTextfield2Label() ?? null,
474            captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
475            infoForAppointment: $matchingScope->getInfoForAppointment() ?? null,
476            infoForAllAppointments: $matchingScope->getInfoForAllAppointments() ?? null,
477            slotsPerAppointment: ((string) $matchingScope->getSlotsPerAppointment() === '' ? null : (string) $matchingScope->getSlotsPerAppointment()) ?? null,
478            appointmentsPerMail: ((string) $matchingScope->getAppointmentsPerMail() === '' ? null : (string) $matchingScope->getAppointmentsPerMail()) ?? null,
479            whitelistedMails: ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()) ?? null,
480            reservationDuration: (int) MapperService::extractReservationDuration($matchingScope),
481            activationDuration: MapperService::extractActivationDuration($matchingScope),
482            hint: ((string) $matchingScope->getScopeHint() === '' ? null : (string) $matchingScope->getScopeHint()) ?? null
483        );
484    }
485
486    public static function getServicesByOfficeId(int $officeId, bool $showUnpublished = false): ServiceList|array
487    {
488        $cacheKey = self::CACHE_KEY_SERVICES_BY_OFFICE_PREFIX . $officeId . ($showUnpublished ? '_unpublished' : '');
489
490        if (\App::$cache && ($cachedData = \App::$cache->get($cacheKey))) {
491            return $cachedData;
492        }
493
494        $requestList = ZmsApiClientService::getServices() ?? new RequestList();
495        $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
496        $requestMap = [];
497        foreach ($requestList as $request) {
498            $additionalData = $request->getAdditionalData();
499            if (
500                !$showUnpublished
501                && isset($additionalData['public'])
502                && !$additionalData['public']
503            ) {
504                continue;
505            }
506
507            $requestMap[$request->id] = $request;
508        }
509
510        $services = [];
511        foreach ($requestRelationList as $relation) {
512            if ((int) $relation->provider->id === $officeId) {
513                $requestId = $relation->request->id;
514                if (isset($requestMap[$requestId])) {
515                    $request = $requestMap[$requestId];
516                    $services[] = new Service(id: (int) $request->id, name: $request->name, maxQuantity: $request->getAdditionalData()['maxQuantity'] ?? 1);
517                }
518            }
519        }
520
521        $errors = ValidationService::validateServicesNotFound($services);
522        if (is_array($errors) && !empty($errors['errors'])) {
523            return $errors;
524        }
525
526        $result = new ServiceList($services);
527
528        self::setMappedCache($cacheKey, $result);
529
530        return $result;
531    }
532
533    /**
534     * @SuppressWarnings(PHPMD.NPathComplexity)
535     */
536    public static function getBookableFreeDays(
537        array $officeIds,
538        array $serviceIds,
539        array $serviceCounts,
540        string $startDate,
541        string $endDate,
542        ?bool $groupByOffice = false
543    ): AvailableDays|AvailableDaysByOffice|array {
544        $firstDay = DateTimeFormatHelper::getInternalDateFromISO($startDate);
545        $lastDay  = DateTimeFormatHelper::getInternalDateFromISO($endDate);
546
547        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
548        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
549
550        $providerSource = [];
551        foreach ($providerList as $p) {
552            $providerSource[(string)$p->id] = (string)($p->source ?? '');
553        }
554
555        $requestSource = [];
556        foreach ($requestList as $r) {
557            $requestSource[(string)$r->id] = (string)($r->source ?? '');
558        }
559
560        $services = [];
561        foreach ($serviceIds as $i => $serviceId) {
562            $sid = (string)$serviceId;
563            $src = $requestSource[$sid] ?? null;
564            if (!$src) {
565                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
566            }
567            $services[] = [
568                'id'        => $serviceId,
569                'source'    => $src,
570                'slotCount' => (int)($serviceCounts[$i] ?? 1),
571            ];
572        }
573
574        $providers = [];
575        foreach ($officeIds as $officeId) {
576            $oid = (string)$officeId;
577            $src = $providerSource[$oid] ?? null;
578            if (!$src) {
579                return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
580            }
581            $providers[] = [
582                'id'     => $officeId,
583                'source' => $src,
584            ];
585        }
586
587        $freeDays = ZmsApiClientService::getFreeDays(
588            new ProviderList($providers),
589            new RequestList($services),
590            $firstDay,
591            $lastDay
592        ) ?? new Calendar();
593
594        $daysCollection  = $freeDays->days;
595        $formattedDays   = [];
596        $scopeToProvider = [];
597
598        foreach ($freeDays->scopes as $scope) {
599            $scopeToProvider[$scope['id']] = $scope['provider']['id'];
600        }
601
602        foreach ($daysCollection as $day) {
603            $formattedDays[] = [
604                'time'        => sprintf('%04d-%02d-%02d', $day->year, $day->month, $day->day),
605                'providerIDs' => isset($day->scopeIDs)
606                    ? implode(',', array_map(fn($scopeId) => $scopeToProvider[$scopeId], explode(',', $day->scopeIDs)))
607                    : ''
608            ];
609        }
610
611        $errors = ValidationService::validateAppointmentDaysNotFound($formattedDays);
612        if (is_array($errors) && !empty($errors['errors'])) {
613            return $errors;
614        }
615
616        return $groupByOffice
617            ? new AvailableDaysByOffice($formattedDays)
618            : new AvailableDays(array_column($formattedDays, 'time'));
619    }
620
621    public static function getFreeAppointments(int $officeId, array $serviceIds, array $serviceCounts, array $date): ProcessList|array
622    {
623        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
624        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
625
626        $providerSource = [];
627        foreach ($providerList as $p) {
628            $providerSource[(string)$p->id] = (string)($p->source ?? '');
629        }
630        $requestSource  = [];
631        foreach ($requestList as $r) {
632            $requestSource[(string)$r->id] = (string)($r->source ?? '');
633        }
634
635        $oid = (string)$officeId;
636        $provSrc = $providerSource[$oid] ?? null;
637        if (!$provSrc) {
638            return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
639        }
640
641        $office = ['id' => $officeId, 'source' => $provSrc];
642
643        $requests = [];
644        foreach ($serviceIds as $id => $serviceId) {
645            $sid   = (string)$serviceId;
646            $reqSrc = $requestSource[$sid] ?? null;
647            if (!$reqSrc) {
648                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
649            }
650            $count = (int)($serviceCounts[$id] ?? 1);
651            for ($k = 0; $k < $count; $k++) {
652                $requests[] = ['id' => $serviceId, 'source' => $reqSrc];
653            }
654        }
655
656        return ZmsApiClientService::getFreeTimeslots(
657            new ProviderList([$office]),
658            new RequestList($requests),
659            $date,
660            $date
661        );
662    }
663
664    /**
665     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
666     */
667    private static function processFreeSlots(ProcessList $freeSlots, bool $groupByOffice = false): array
668    {
669        $errors = ValidationService::validateGetProcessFreeSlots($freeSlots);
670        if (is_array($errors) && !empty($errors['errors'])) {
671            return $errors;
672        }
673
674        $currentTimestamp = time();
675        if ($groupByOffice) {
676            $grouped = [];
677            foreach ($freeSlots as $slot) {
678                $officeId = (string)($slot->scope->provider->id ?? '');
679                if (!isset($grouped[$officeId])) {
680                    $grouped[$officeId] = [];
681                }
682                if (isset($slot->appointments) && is_iterable($slot->appointments)) {
683                    foreach ($slot->appointments as $appointment) {
684                        if (isset($appointment->date)) {
685                            $timestamp = (int) $appointment->date;
686                            if ($timestamp > $currentTimestamp) {
687                                $grouped[$officeId][] = $timestamp;
688                            }
689                        }
690                    }
691                }
692            }
693            // Sort each office's appointments
694            foreach ($grouped as &$arr) {
695                sort($arr);
696            }
697            unset($arr);
698            // Optionally validate grouped timestamps here if needed
699            return $grouped;
700        } else {
701            $timestamps = [];
702            foreach ($freeSlots as $slot) {
703                if (isset($slot->appointments) && is_iterable($slot->appointments)) {
704                    foreach ($slot->appointments as $appointment) {
705                        if (isset($appointment->date)) {
706                            $timestamp = (int) $appointment->date;
707                            if ($timestamp > $currentTimestamp) {
708                                $timestamps[] = $timestamp;
709                            }
710                        }
711                    }
712                }
713            }
714            sort($timestamps);
715
716            $errors = ValidationService::validateGetProcessByIdTimestamps($timestamps);
717            if (is_array($errors) && !empty($errors['errors'])) {
718                return $errors;
719            }
720
721            return $timestamps;
722        }
723    }
724
725    public static function getAvailableAppointments(
726        string $date,
727        array $officeIds,
728        array $serviceIds,
729        array $serviceCounts,
730        ?bool $groupByOffice = false
731    ): AvailableAppointments|AvailableAppointmentsByOffice|array {
732        $providerList = ZmsApiClientService::getOffices()  ?? new ProviderList();
733        $requestList  = ZmsApiClientService::getServices() ?? new RequestList();
734
735        $providerSource = [];
736        foreach ($providerList as $p) {
737            $providerSource[(string)$p->id] = (string)($p->source ?? '');
738        }
739        $requestSource  = [];
740        foreach ($requestList as $r) {
741            $requestSource[(string)$r->id] = (string)($r->source ?? '');
742        }
743
744        $requests = [];
745        foreach ($serviceIds as $id => $serviceId) {
746            $sid = (string)$serviceId;
747            $src = $requestSource[$sid] ?? null;
748            if (!$src) {
749                return ['errors' => [['message' => 'Unknown service source for ID ' . $sid]]];
750            }
751            $count = (int)($serviceCounts[$id] ?? 1);
752            for ($k = 0; $k < $count; $k++) {
753                $requests[] = ['id' => $serviceId, 'source' => $src];
754            }
755        }
756
757        $providers = [];
758        foreach ($officeIds as $officeId) {
759            $oid = (string)$officeId;
760            $src = $providerSource[$oid] ?? null;
761            if (!$src) {
762                return ['errors' => [['message' => 'Unknown provider source for ID ' . $oid]]];
763            }
764            $providers[] = ['id' => $officeId, 'source' => $src];
765        }
766
767        $freeSlots = ZmsApiClientService::getFreeTimeslots(
768            new ProviderList($providers),
769            new RequestList($requests),
770            DateTimeFormatHelper::getInternalDateFromISO($date),
771            DateTimeFormatHelper::getInternalDateFromISO($date)
772        ) ?? new ProcessList();
773
774        $result = self::processFreeSlots($freeSlots, $groupByOffice);
775        if (isset($result['errors']) && !empty($result['errors'])) {
776            return $result;
777        }
778
779        return $groupByOffice
780            ? new AvailableAppointmentsByOffice($result)
781            : new AvailableAppointments($result);
782    }
783
784    public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): ThinnedProcess|array
785    {
786        $errors = ValidationService::validateServiceArrays($serviceIds, $serviceCounts);
787        if (!empty($errors)) {
788            return $errors;
789        }
790        $process = ZmsApiClientService::reserveTimeslot($appointmentProcess, $serviceIds, $serviceCounts);
791        return MapperService::processToThinnedProcess($process);
792    }
793
794    public static function getThinnedProcessById(?int $processId, ?string $authKey): ThinnedProcess|array
795    {
796        $process = ZmsApiClientService::getProcessById($processId, $authKey);
797        $errors = ValidationService::validateGetProcessNotFound($process);
798        if (is_array($errors) && !empty($errors['errors'])) {
799            return $errors;
800        }
801        $thinnedProcess = MapperService::processToThinnedProcess($process);
802
803        $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
804        $providerMap = [];
805        foreach ($providerList as $provider) {
806            $key = $provider->getSource() . '_' . $provider->id;
807            $providerMap[$key] = $provider;
808        }
809
810        $thinnedScope = null;
811        if ($process->scope instanceof Scope) {
812            $scopeProvider = $process->scope->getProvider();
813            $providerKey = $scopeProvider ? ($scopeProvider->getSource() . '_' . $scopeProvider->id) : null;
814            $matchingProvider = $providerKey && isset($providerMap[$providerKey]) ? $providerMap[$providerKey] : $scopeProvider;
815            $thinnedProvider = MapperService::providerToThinnedProvider($matchingProvider);
816            $thinnedScope = new ThinnedScope(
817                id: (int) $process->scope->id,
818                provider: $thinnedProvider,
819                shortName: (string) $process->scope->getShortName() ?? null,
820                emailFrom: (string) $process->scope->getEmailFrom() ?? null,
821                emailRequired: (bool) $process->scope->getEmailRequired() ?? false,
822                telephoneActivated: (bool) $process->scope->getTelephoneActivated() ?? false,
823                telephoneRequired: (bool) $process->scope->getTelephoneRequired() ?? false,
824                customTextfieldActivated: (bool) $process->scope->getCustomTextfieldActivated() ?? false,
825                customTextfieldRequired: (bool) $process->scope->getCustomTextfieldRequired() ?? false,
826                customTextfieldLabel: $process->scope->getCustomTextfieldLabel() ?? null,
827                customTextfield2Activated: (bool) $process->scope->getCustomTextfield2Activated() ?? false,
828                customTextfield2Required: (bool) $process->scope->getCustomTextfield2Required() ?? false,
829                customTextfield2Label: $process->scope->getCustomTextfield2Label() ?? null,
830                captchaActivatedRequired: (bool) $process->scope->getCaptchaActivatedRequired() ?? false,
831                infoForAppointment: $process->scope->getInfoForAppointment() ?? null,
832                infoForAllAppointments: $process->scope->getInfoForAllAppointments() ?? null,
833                slotsPerAppointment: ((string) $process->scope->getSlotsPerAppointment() === '' ? null : (string) $process->scope->getSlotsPerAppointment()) ?? null,
834                appointmentsPerMail: ((string) $process->scope->getAppointmentsPerMail() === '' ? null : (string) $process->scope->getAppointmentsPerMail()) ?? null,
835                whitelistedMails: ((string) $process->scope->getWhitelistedMails() === '' ? null : (string) $process->scope->getWhitelistedMails()) ?? null,
836                reservationDuration: (int) MapperService::extractReservationDuration($process->scope),
837                activationDuration: MapperService::extractActivationDuration($process->scope),
838                hint: ((string) $process->scope->getScopeHint() === '' ? null : (string) $process->scope->getScopeHint()) ?? null
839            );
840        }
841
842        $thinnedProcess->scope = $thinnedScope;
843        return $thinnedProcess;
844    }
845
846    public static function updateClientData(Process $reservedProcess): Process|array
847    {
848        $clientUpdateResult = ZmsApiClientService::submitClientData($reservedProcess);
849        if (isset($clientUpdateResult['error'])) {
850            return $clientUpdateResult;
851        }
852        return $clientUpdateResult;
853    }
854
855    public static function preconfirmAppointment(Process $reservedProcess): Process|array
856    {
857        $clientUpdateResult = ZmsApiClientService::preconfirmProcess($reservedProcess);
858        if (isset($clientUpdateResult['error'])) {
859            return $clientUpdateResult;
860        }
861        return $clientUpdateResult;
862    }
863
864    public static function confirmAppointment(Process $preconfirmedProcess): Process|array
865    {
866        $clientUpdateResult = ZmsApiClientService::confirmProcess($preconfirmedProcess);
867        if (isset($clientUpdateResult['error'])) {
868            return $clientUpdateResult;
869        }
870        return $clientUpdateResult;
871    }
872
873    public static function cancelAppointment(Process $confirmedProcess): Process|array
874    {
875        $clientUpdateResult = ZmsApiClientService::cancelAppointment($confirmedProcess);
876        if (isset($clientUpdateResult['error'])) {
877            return $clientUpdateResult;
878        }
879        return $clientUpdateResult;
880    }
881
882    public static function sendPreconfirmationEmail(Process $reservedProcess): Process|array
883    {
884        $clientUpdateResult = ZmsApiClientService::sendPreconfirmationEmail($reservedProcess);
885        if (isset($clientUpdateResult['error'])) {
886            return $clientUpdateResult;
887        }
888        return $clientUpdateResult;
889    }
890
891    public static function sendConfirmationEmail(Process $preconfirmedProcess): Process|array
892    {
893        $clientUpdateResult = ZmsApiClientService::sendConfirmationEmail($preconfirmedProcess);
894        if (isset($clientUpdateResult['error'])) {
895            return $clientUpdateResult;
896        }
897        return $clientUpdateResult;
898    }
899
900    public static function sendCancellationEmail(Process $confirmedProcess): Process|array
901    {
902        $clientUpdateResult = ZmsApiClientService::sendCancellationEmail($confirmedProcess);
903        if (isset($clientUpdateResult['error'])) {
904            return $clientUpdateResult;
905        }
906        return $clientUpdateResult;
907    }
908}