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