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