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