Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.57% covered (warning)
83.57%
234 / 280
68.18% covered (warning)
68.18%
15 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZmsApiClientService
83.57% covered (warning)
83.57%
234 / 280
68.18% covered (warning)
68.18%
15 / 22
151.96
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 / 10
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\Zmscitizenapi\Utils\ClientIpHelper;
8use BO\Zmsentities\Calendar;
9use BO\Zmsentities\Process;
10use BO\Zmsentities\Source;
11use BO\Zmsentities\Collection\DayList;
12use BO\Zmsentities\Collection\ProcessList;
13use BO\Zmsentities\Collection\ProviderList;
14use BO\Zmsentities\Collection\RequestList;
15use BO\Zmsentities\Collection\RequestRelationList;
16use BO\Zmsentities\Collection\ScopeList;
17
18/**
19 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
20 */
21class 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}