Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.22% |
499 / 547 |
|
12.50% |
3 / 24 |
CRAP | |
0.00% |
0 / 1 |
ZmsApiFacadeService | |
91.22% |
499 / 547 |
|
12.50% |
3 / 24 |
206.88 | |
0.00% |
0 / 1 |
setLanguageContext | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setMappedCache | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getOffices | |
96.23% |
51 / 53 |
|
0.00% |
0 / 1 |
18 | |||
getScopes | |
97.67% |
42 / 43 |
|
0.00% |
0 / 1 |
14 | |||
getServices | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
8.02 | |||
getServicesAndOffices | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
getScopeByOfficeId | |
95.89% |
70 / 73 |
|
0.00% |
0 / 1 |
14 | |||
getOfficeListByServiceId | |
92.50% |
37 / 40 |
|
0.00% |
0 / 1 |
16.11 | |||
getScopeById | |
97.83% |
45 / 46 |
|
0.00% |
0 / 1 |
14 | |||
getServicesByOfficeId | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
13 | |||
getBookableFreeDays | |
92.73% |
51 / 55 |
|
0.00% |
0 / 1 |
13.07 | |||
getFreeAppointments | |
89.29% |
25 / 28 |
|
0.00% |
0 / 1 |
7.06 | |||
processFreeSlots | |
51.52% |
17 / 33 |
|
0.00% |
0 / 1 |
65.59 | |||
getAvailableAppointments | |
91.67% |
33 / 36 |
|
0.00% |
0 / 1 |
11.07 | |||
reserveTimeslot | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
getThinnedProcessById | |
97.62% |
41 / 42 |
|
0.00% |
0 / 1 |
12 | |||
updateClientData | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
preconfirmAppointment | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
confirmAppointment | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
cancelAppointment | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
sendPreconfirmationEmail | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
sendConfirmationEmail | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
sendCancellationEmail | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace BO\Zmscitizenapi\Services\Core; |
6 | |
7 | use BO\Zmscitizenapi\Utils\DateTimeFormatHelper; |
8 | use BO\Zmscitizenapi\Utils\ErrorMessages; |
9 | use BO\Zmscitizenapi\Models\AvailableAppointmentsByOffice; |
10 | use BO\Zmscitizenapi\Models\AvailableDays; |
11 | use BO\Zmscitizenapi\Models\AvailableAppointments; |
12 | use BO\Zmscitizenapi\Models\AvailableDaysByOffice; |
13 | use BO\Zmscitizenapi\Models\Office; |
14 | use BO\Zmscitizenapi\Models\Service; |
15 | use BO\Zmscitizenapi\Models\ThinnedProcess; |
16 | use BO\Zmscitizenapi\Models\ThinnedScope; |
17 | use BO\Zmscitizenapi\Models\Collections\OfficeList; |
18 | use BO\Zmscitizenapi\Models\Collections\OfficeServiceRelationList; |
19 | use BO\Zmscitizenapi\Models\Collections\OfficeServiceAndRelationList; |
20 | use BO\Zmscitizenapi\Models\Collections\ServiceList; |
21 | use BO\Zmscitizenapi\Models\Collections\ThinnedScopeList; |
22 | use BO\Zmscitizenapi\Services\Core\ZmsApiClientService; |
23 | use BO\Zmsentities\Calendar; |
24 | use BO\Zmsentities\Collection\RequestRelationList; |
25 | use BO\Zmsentities\Process; |
26 | use BO\Zmsentities\Scope; |
27 | use BO\Zmsentities\Collection\ScopeList; |
28 | use BO\Zmsentities\Collection\ProviderList; |
29 | use BO\Zmsentities\Collection\RequestList; |
30 | use 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 | */ |
36 | class 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 | } |