Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.57% |
234 / 280 |
|
68.18% |
15 / 22 |
CRAP | |
0.00% |
0 / 1 |
ZmsApiClientService | |
83.57% |
234 / 280 |
|
68.18% |
15 / 22 |
151.96 | |
0.00% |
0 / 1 |
getMergedMailTemplates | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
156 | |||
getIcsContent | |
50.00% |
6 / 12 |
|
0.00% |
0 / 1 |
4.12 | |||
getOffices | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
getServices | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
getRequestRelationList | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
6 | |||
getScopes | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
getFreeDays | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
6 | |||
getFreeTimeslots | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
reserveTimeslot | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
8 | |||
submitClientData | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
preconfirmProcess | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
confirmProcess | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
cancelAppointment | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
sendConfirmationEmail | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
sendPreconfirmationEmail | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
sendCancellationEmail | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getProcessById | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getProcessByIdAuthenticated | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getScopesByProviderId | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
fetchSourceDataFor | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
getSourceNames | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
7.04 | |||
getProcessesByExternalUserId | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
5.20 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace BO\Zmscitizenapi\Services\Core; |
6 | |
7 | use BO\Zmscitizenapi\Utils\ClientIpHelper; |
8 | use BO\Zmsentities\Calendar; |
9 | use BO\Zmsentities\Process; |
10 | use BO\Zmsentities\Source; |
11 | use BO\Zmsentities\Collection\DayList; |
12 | use BO\Zmsentities\Collection\ProcessList; |
13 | use BO\Zmsentities\Collection\ProviderList; |
14 | use BO\Zmsentities\Collection\RequestList; |
15 | use BO\Zmsentities\Collection\RequestRelationList; |
16 | use BO\Zmsentities\Collection\ScopeList; |
17 | |
18 | /** |
19 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
20 | */ |
21 | class ZmsApiClientService |
22 | { |
23 | public static function getMergedMailTemplates(int $providerId): array |
24 | { |
25 | try { |
26 | $cacheKey = 'merged_mailtemplates_' . $providerId; |
27 | if (\App::$cache && ($cached = \App::$cache->get($cacheKey))) { |
28 | return is_array($cached) ? $cached : []; |
29 | } |
30 | $result = \App::$http->readGetResult('/merged-mailtemplates/' . $providerId . '/'); |
31 | $templates = $result?->getCollection(); |
32 | if (!is_iterable($templates)) { |
33 | return []; |
34 | } |
35 | $out = []; |
36 | foreach ($templates as $template) { |
37 | $name = is_array($template) ? ($template['name'] ?? null) : ($template->name ?? null); |
38 | $value = is_array($template) ? ($template['value'] ?? null) : ($template->value ?? null); |
39 | if ($name !== null && $value !== null) { |
40 | $out[(string)$name] = (string)$value; |
41 | } |
42 | } |
43 | if (\App::$cache) { |
44 | \App::$cache->set($cacheKey, $out, \App::$SOURCE_CACHE_TTL); |
45 | LoggerService::logInfo('Cache set', [ |
46 | 'key' => $cacheKey, |
47 | 'ttl' => \App::$SOURCE_CACHE_TTL, |
48 | 'entity_type' => 'merged_mail_templates' |
49 | ]); |
50 | } |
51 | return $out; |
52 | } catch (\Exception $e) { |
53 | ExceptionService::handleException($e); |
54 | } |
55 | } |
56 | |
57 | public static function getIcsContent(int $processId, string $authKey): ?string |
58 | { |
59 | try { |
60 | $url = "/process/{$processId}/{$authKey}/ics/"; |
61 | $result = \App::$http->readGetResult($url); |
62 | $entity = $result?->getEntity(); |
63 | if ($entity instanceof \BO\Zmsentities\Ics) { |
64 | return $entity->getContent() ?? null; |
65 | } |
66 | return null; |
67 | } catch (\Exception $e) { |
68 | // Do not fail the user flow if ICS is unavailable; just log and return null |
69 | LoggerService::logError($e, null, null, [ |
70 | 'processId' => $processId, |
71 | 'context' => 'ICS fetch via API' |
72 | ]); |
73 | return null; |
74 | } |
75 | } |
76 | public static function getOffices(): ProviderList |
77 | { |
78 | try { |
79 | $combined = new ProviderList(); |
80 | $seen = []; |
81 | |
82 | foreach (self::getSourceNames() as $name) { |
83 | $src = self::fetchSourceDataFor($name); |
84 | $list = $src?->getProviderList(); |
85 | |
86 | if ($list instanceof ProviderList) { |
87 | foreach ($list as $provider) { |
88 | $key = (($provider->source ?? '') . '_' . $provider->id); |
89 | if (!isset($seen[$key])) { |
90 | $combined->addEntity($provider); |
91 | $seen[$key] = true; |
92 | } |
93 | } |
94 | } |
95 | } |
96 | |
97 | return $combined; |
98 | } catch (\Exception $e) { |
99 | ExceptionService::handleException($e); |
100 | } |
101 | } |
102 | |
103 | public static function getServices(): RequestList |
104 | { |
105 | try { |
106 | $combined = new RequestList(); |
107 | $seen = []; |
108 | |
109 | foreach (self::getSourceNames() as $name) { |
110 | $src = self::fetchSourceDataFor($name); |
111 | $list = $src?->getRequestList(); |
112 | |
113 | if ($list instanceof RequestList) { |
114 | foreach ($list as $request) { |
115 | $key = (($request->source ?? '') . '_' . $request->id); |
116 | if (!isset($seen[$key])) { |
117 | $combined->addEntity($request); |
118 | $seen[$key] = true; |
119 | } |
120 | } |
121 | } |
122 | } |
123 | |
124 | return $combined; |
125 | } catch (\Exception $e) { |
126 | ExceptionService::handleException($e); |
127 | } |
128 | } |
129 | |
130 | public static function getRequestRelationList(): RequestRelationList |
131 | { |
132 | try { |
133 | $combined = new RequestRelationList(); |
134 | $seen = []; |
135 | |
136 | foreach (self::getSourceNames() as $name) { |
137 | $src = self::fetchSourceDataFor($name); |
138 | $list = $src?->getRequestRelationList(); |
139 | |
140 | if ($list instanceof RequestRelationList) { |
141 | foreach ($list as $rel) { |
142 | $r = $rel->request ?? null; |
143 | $p = $rel->provider ?? null; |
144 | |
145 | $key = (($r->source ?? '') . '_' . $r->id) . '|' . (($p->source ?? '') . '_' . $p->id); |
146 | if (!isset($seen[$key])) { |
147 | $combined->addEntity($rel); |
148 | $seen[$key] = true; |
149 | } |
150 | } |
151 | } |
152 | } |
153 | |
154 | return $combined; |
155 | } catch (\Exception $e) { |
156 | ExceptionService::handleException($e); |
157 | } |
158 | } |
159 | |
160 | public static function getScopes(): ScopeList |
161 | { |
162 | try { |
163 | $combined = new ScopeList(); |
164 | $seen = []; |
165 | |
166 | foreach (self::getSourceNames() as $name) { |
167 | $src = self::fetchSourceDataFor($name); |
168 | $list = $src?->getScopeList(); |
169 | |
170 | if ($list instanceof ScopeList) { |
171 | foreach ($list as $scope) { |
172 | $prov = $scope->getProvider(); |
173 | $key = (($prov->source ?? '') . '_' . $prov->id); |
174 | if (!isset($seen[$key])) { |
175 | $combined->addEntity($scope); |
176 | $seen[$key] = true; |
177 | } |
178 | } |
179 | } |
180 | } |
181 | |
182 | return $combined; |
183 | } catch (\Exception $e) { |
184 | ExceptionService::handleException($e); |
185 | } |
186 | } |
187 | |
188 | public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar |
189 | { |
190 | try { |
191 | $calendar = new Calendar(); |
192 | $calendar->firstDay = $firstDay; |
193 | $calendar->lastDay = $lastDay; |
194 | $calendar->providers = $providers; |
195 | $calendar->requests = $requests; |
196 | $result = \App::$http->readPostResult('/calendar/', $calendar); |
197 | $entity = $result?->getEntity(); |
198 | |
199 | if (!$entity instanceof Calendar) { |
200 | return new Calendar(); |
201 | } |
202 | $bookableDays = new DayList(); |
203 | foreach ($entity->days as $day) { |
204 | if (isset($day['status']) && $day['status'] === 'bookable') { |
205 | $bookableDays->addEntity($day); |
206 | } |
207 | } |
208 | $entity->days = $bookableDays; |
209 | |
210 | return $entity; |
211 | } catch (\Exception $e) { |
212 | ExceptionService::handleException($e); |
213 | } |
214 | } |
215 | |
216 | public static function getFreeTimeslots(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): ProcessList |
217 | { |
218 | try { |
219 | $calendar = new Calendar(); |
220 | $calendar->firstDay = $firstDay; |
221 | $calendar->lastDay = $lastDay; |
222 | $calendar->providers = $providers; |
223 | $calendar->requests = $requests; |
224 | $result = \App::$http->readPostResult('/process/status/free/unique/', $calendar); |
225 | $collection = $result?->getCollection(); |
226 | if (!$collection instanceof ProcessList) { |
227 | return new ProcessList(); |
228 | } |
229 | |
230 | return $collection; |
231 | } catch (\Exception $e) { |
232 | ExceptionService::handleException($e); |
233 | } |
234 | } |
235 | |
236 | public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process |
237 | { |
238 | try { |
239 | $requestList = self::getServices() ?? new RequestList(); |
240 | $requestSource = []; |
241 | foreach ($requestList as $r) { |
242 | $requestSource[(string)$r->id] = (string)($r->source ?? ''); |
243 | } |
244 | |
245 | $requests = []; |
246 | foreach ($serviceIds as $index => $serviceId) { |
247 | $sid = (string)$serviceId; |
248 | $src = $requestSource[$sid] ?? null; |
249 | if (!$src) { |
250 | return new Process(); |
251 | } |
252 | $count = (int)($serviceCounts[$index] ?? 1); |
253 | for ($i = 0; $i < $count; $i++) { |
254 | $requests[] = ['id' => $serviceId, 'source' => $src]; |
255 | } |
256 | } |
257 | |
258 | $processEntity = new Process(); |
259 | $processEntity->appointments = $appointmentProcess->appointments ?? []; |
260 | $processEntity->authKey = $appointmentProcess->authKey ?? null; |
261 | $processEntity->clients = $appointmentProcess->clients ?? []; |
262 | $processEntity->scope = $appointmentProcess->scope ?? null; |
263 | $processEntity->requests = $requests; |
264 | $processEntity->lastChange = $appointmentProcess->lastChange ?? time(); |
265 | $processEntity->createIP = ClientIpHelper::getClientIp(); |
266 | $processEntity->createTimestamp = time(); |
267 | if (isset($appointmentProcess->queue)) { |
268 | $processEntity->queue = $appointmentProcess->queue; |
269 | } |
270 | |
271 | $result = \App::$http->readPostResult('/process/status/reserved/', $processEntity); |
272 | $entity = $result?->getEntity(); |
273 | return $entity instanceof Process ? $entity : new Process(); |
274 | } catch (\Exception $e) { |
275 | ExceptionService::handleException($e); |
276 | } |
277 | } |
278 | |
279 | public static function submitClientData(Process $process): Process |
280 | { |
281 | try { |
282 | $url = "/process/{$process->id}/{$process->authKey}/"; |
283 | $result = \App::$http->readPostResult($url, $process); |
284 | $entity = $result?->getEntity(); |
285 | if (!$entity instanceof Process) { |
286 | return new Process(); |
287 | } |
288 | return $entity; |
289 | } catch (\Exception $e) { |
290 | ExceptionService::handleException($e); |
291 | } |
292 | } |
293 | |
294 | public static function preconfirmProcess(Process $process): Process |
295 | { |
296 | try { |
297 | $url = '/process/status/preconfirmed/'; |
298 | $result = \App::$http->readPostResult($url, $process); |
299 | $entity = $result?->getEntity(); |
300 | if (!$entity instanceof Process) { |
301 | return new Process(); |
302 | } |
303 | return $entity; |
304 | } catch (\Exception $e) { |
305 | ExceptionService::handleException($e); |
306 | } |
307 | } |
308 | |
309 | public static function confirmProcess(Process $process): Process |
310 | { |
311 | try { |
312 | $url = '/process/status/confirmed/'; |
313 | $result = \App::$http->readPostResult($url, $process); |
314 | $entity = $result?->getEntity(); |
315 | if (!$entity instanceof Process) { |
316 | return new Process(); |
317 | } |
318 | return $entity; |
319 | } catch (\Exception $e) { |
320 | ExceptionService::handleException($e); |
321 | } |
322 | } |
323 | |
324 | public static function cancelAppointment(Process $process): Process |
325 | { |
326 | try { |
327 | $url = "/process/{$process->id}/{$process->authKey}/"; |
328 | $result = \App::$http->readDeleteResult($url, []); |
329 | $entity = $result?->getEntity(); |
330 | if (!$entity instanceof Process) { |
331 | return new Process(); |
332 | } |
333 | return $entity; |
334 | } catch (\Exception $e) { |
335 | ExceptionService::handleException($e); |
336 | } |
337 | } |
338 | |
339 | public static function sendConfirmationEmail(Process $process): Process |
340 | { |
341 | try { |
342 | $url = "/process/{$process->id}/{$process->authKey}/confirmation/mail/"; |
343 | $result = \App::$http->readPostResult($url, $process); |
344 | $entity = $result?->getEntity(); |
345 | if (!$entity instanceof Process) { |
346 | return new Process(); |
347 | } |
348 | return $entity; |
349 | } catch (\Exception $e) { |
350 | ExceptionService::handleException($e); |
351 | } |
352 | } |
353 | |
354 | public static function sendPreconfirmationEmail(Process $process): Process |
355 | { |
356 | try { |
357 | $url = "/process/{$process->id}/{$process->authKey}/preconfirmation/mail/"; |
358 | $result = \App::$http->readPostResult($url, $process); |
359 | $entity = $result?->getEntity(); |
360 | if (!$entity instanceof Process) { |
361 | return new Process(); |
362 | } |
363 | return $entity; |
364 | } catch (\Exception $e) { |
365 | ExceptionService::handleException($e); |
366 | } |
367 | } |
368 | |
369 | public static function sendCancellationEmail(Process $process): Process |
370 | { |
371 | try { |
372 | $url = "/process/{$process->id}/{$process->authKey}/delete/mail/"; |
373 | $result = \App::$http->readPostResult($url, $process); |
374 | $entity = $result?->getEntity(); |
375 | if (!$entity instanceof Process) { |
376 | return new Process(); |
377 | } |
378 | return $entity; |
379 | } catch (\Exception $e) { |
380 | ExceptionService::handleException($e); |
381 | } |
382 | } |
383 | |
384 | public static function getProcessById(int $processId, string $authKey): Process |
385 | { |
386 | try { |
387 | $resolveReferences = 2; |
388 | $result = \App::$http->readGetResult("/process/{$processId}/{$authKey}/", [ |
389 | 'resolveReferences' => $resolveReferences |
390 | ]); |
391 | $entity = $result?->getEntity(); |
392 | if (!$entity instanceof Process) { |
393 | return new Process(); |
394 | } |
395 | return $entity; |
396 | } catch (\Exception $e) { |
397 | ExceptionService::handleException($e); |
398 | } |
399 | } |
400 | |
401 | public static function getProcessByIdAuthenticated(int $processId): Process |
402 | { |
403 | try { |
404 | $resolveReferences = 2; |
405 | // This endpoint is normally reserved for workstation (zmsadmin) Users. |
406 | $result = \App::$http->readGetResult("/process/{$processId}/", [ |
407 | 'resolveReferences' => $resolveReferences |
408 | ]); |
409 | $entity = $result?->getEntity(); |
410 | if (!$entity instanceof Process) { |
411 | return new Process(); |
412 | } |
413 | return $entity; |
414 | } catch (\Exception $e) { |
415 | ExceptionService::handleException($e); |
416 | } |
417 | } |
418 | |
419 | public static function getScopesByProviderId(string $source, string|int $providerId): ScopeList |
420 | { |
421 | try { |
422 | $scopeList = self::getScopes(); |
423 | if (!$scopeList instanceof ScopeList) { |
424 | return new ScopeList(); |
425 | } |
426 | $result = $scopeList->withProviderID($source, (string)$providerId); |
427 | if (!$result instanceof ScopeList) { |
428 | return new ScopeList(); |
429 | } |
430 | return $result; |
431 | } catch (\Exception $e) { |
432 | ExceptionService::handleException($e); |
433 | } |
434 | } |
435 | |
436 | private static function fetchSourceDataFor(string $sourceName): Source |
437 | { |
438 | $cacheKey = 'source_' . $sourceName; |
439 | if (\App::$cache && ($data = \App::$cache->get($cacheKey))) { |
440 | return $data; |
441 | } |
442 | |
443 | $result = \App::$http->readGetResult('/source/' . $sourceName . '/', [ |
444 | 'resolveReferences' => 2, |
445 | ]); |
446 | $entity = $result?->getEntity(); |
447 | if (!$entity instanceof Source) { |
448 | return new Source(); |
449 | } |
450 | |
451 | if (\App::$cache) { |
452 | \App::$cache->set($cacheKey, $entity, \App::$SOURCE_CACHE_TTL); |
453 | LoggerService::logInfo('Cache set', [ |
454 | 'key' => $cacheKey, |
455 | 'ttl' => \App::$SOURCE_CACHE_TTL, |
456 | 'entity_type' => get_class($entity) |
457 | ]); |
458 | } |
459 | |
460 | return $entity; |
461 | } |
462 | |
463 | /** |
464 | * Akzeptiert sowohl: |
465 | * - String: "dldb", "dldb,zms", "dldb; zms", "dldb zms", "dldb|zms" |
466 | * - Array: ["dldb","zms"] |
467 | */ |
468 | private static function getSourceNames(): array |
469 | { |
470 | $raw = \App::$source_name ?? 'dldb'; |
471 | |
472 | if (is_array($raw)) { |
473 | $names = array_values(array_filter(array_map('strval', $raw))); |
474 | } else { |
475 | $s = (string)$raw; |
476 | $names = preg_split('/[,\;\|\s]+/', $s, -1, PREG_SPLIT_NO_EMPTY) ?: []; |
477 | } |
478 | |
479 | $out = []; |
480 | foreach ($names as $n) { |
481 | $n = trim($n); |
482 | if ($n !== '' && !in_array($n, $out, true)) { |
483 | $out[] = $n; |
484 | } |
485 | } |
486 | |
487 | return $out ?: ['dldb']; |
488 | } |
489 | |
490 | public static function getProcessesByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null): ProcessList |
491 | { |
492 | try { |
493 | $params = [ |
494 | 'resolveReferences' => 2, |
495 | ]; |
496 | if (!is_null($filterId)) { |
497 | $params['filterId'] = $filterId; |
498 | } |
499 | if (!is_null($status)) { |
500 | $params['status'] = $status; |
501 | } |
502 | $externalUserIdUrlEncoded = urlencode($externalUserId); |
503 | $result = \App::$http->readGetResult("/process/externaluserid/{$externalUserIdUrlEncoded}/", $params); |
504 | $collection = $result?->getCollection(); |
505 | if (!$collection instanceof ProcessList) { |
506 | return new ProcessList(); |
507 | } |
508 | return $collection; |
509 | } catch (\Exception $e) { |
510 | ExceptionService::handleException($e); |
511 | } |
512 | } |
513 | } |