Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.18% covered (success)
98.18%
216 / 220
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZmsApiClientService
98.18% covered (success)
98.18%
216 / 220
83.33% covered (warning)
83.33%
15 / 18
81
0.00% covered (danger)
0.00%
0 / 1
 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
 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
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
18class ZmsApiClientService
19{
20    public static function getOffices(): ProviderList
21    {
22        try {
23            $combined = new ProviderList();
24            $seen = [];
25
26            foreach (self::getSourceNames() as $name) {
27                $src = self::fetchSourceDataFor($name);
28                $list = $src?->getProviderList();
29
30                if ($list instanceof ProviderList) {
31                    foreach ($list as $provider) {
32                        $key = (($provider->source ?? '') . '_' . $provider->id);
33                        if (!isset($seen[$key])) {
34                            $combined->addEntity($provider);
35                            $seen[$key] = true;
36                        }
37                    }
38                }
39            }
40
41            return $combined;
42        } catch (\Exception $e) {
43            ExceptionService::handleException($e);
44        }
45    }
46
47    public static function getServices(): RequestList
48    {
49        try {
50            $combined = new RequestList();
51            $seen = [];
52
53            foreach (self::getSourceNames() as $name) {
54                $src = self::fetchSourceDataFor($name);
55                $list = $src?->getRequestList();
56
57                if ($list instanceof RequestList) {
58                    foreach ($list as $request) {
59                        $key = (($request->source ?? '') . '_' . $request->id);
60                        if (!isset($seen[$key])) {
61                            $combined->addEntity($request);
62                            $seen[$key] = true;
63                        }
64                    }
65                }
66            }
67
68            return $combined;
69        } catch (\Exception $e) {
70            ExceptionService::handleException($e);
71        }
72    }
73
74    public static function getRequestRelationList(): RequestRelationList
75    {
76        try {
77            $combined = new RequestRelationList();
78            $seen = [];
79
80            foreach (self::getSourceNames() as $name) {
81                $src = self::fetchSourceDataFor($name);
82                $list = $src?->getRequestRelationList();
83
84                if ($list instanceof RequestRelationList) {
85                    foreach ($list as $rel) {
86                        $r = $rel->request ?? null;
87                        $p = $rel->provider ?? null;
88
89                        $key = (($r->source ?? '') . '_' . $r->id) . '|' . (($p->source ?? '') . '_' . $p->id);
90                        if (!isset($seen[$key])) {
91                            $combined->addEntity($rel);
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 getScopes(): ScopeList
105    {
106        try {
107            $combined = new ScopeList();
108            $seen = [];
109
110            foreach (self::getSourceNames() as $name) {
111                $src = self::fetchSourceDataFor($name);
112                $list = $src?->getScopeList();
113
114                if ($list instanceof ScopeList) {
115                    foreach ($list as $scope) {
116                        $prov = $scope->getProvider();
117                        $key = (($prov->source ?? '') . '_' . $prov->id);
118                        if (!isset($seen[$key])) {
119                            $combined->addEntity($scope);
120                            $seen[$key] = true;
121                        }
122                    }
123                }
124            }
125
126            return $combined;
127        } catch (\Exception $e) {
128            ExceptionService::handleException($e);
129        }
130    }
131
132    public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
133    {
134        try {
135            $calendar = new Calendar();
136            $calendar->firstDay = $firstDay;
137            $calendar->lastDay = $lastDay;
138            $calendar->providers = $providers;
139            $calendar->requests = $requests;
140            $result = \App::$http->readPostResult('/calendar/', $calendar);
141            $entity = $result?->getEntity();
142
143            if (!$entity instanceof Calendar) {
144                return new Calendar();
145            }
146            $bookableDays = new DayList();
147            foreach ($entity->days as $day) {
148                if (isset($day['status']) && $day['status'] === 'bookable') {
149                    $bookableDays->addEntity($day);
150                }
151            }
152            $entity->days = $bookableDays;
153
154            return $entity;
155        } catch (\Exception $e) {
156            ExceptionService::handleException($e);
157        }
158    }
159
160    public static function getFreeTimeslots(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): ProcessList
161    {
162        try {
163            $calendar = new Calendar();
164            $calendar->firstDay = $firstDay;
165            $calendar->lastDay = $lastDay;
166            $calendar->providers = $providers;
167            $calendar->requests = $requests;
168            $result = \App::$http->readPostResult('/process/status/free/unique/', $calendar);
169            $collection = $result?->getCollection();
170            if (!$collection instanceof ProcessList) {
171                return new ProcessList();
172            }
173
174            return $collection;
175        } catch (\Exception $e) {
176            ExceptionService::handleException($e);
177        }
178    }
179
180    public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
181    {
182        try {
183            $requestList = self::getServices() ?? new RequestList();
184            $requestSource = [];
185            foreach ($requestList as $r) {
186                $requestSource[(string)$r->id] = (string)($r->source ?? '');
187            }
188
189            $requests = [];
190            foreach ($serviceIds as $index => $serviceId) {
191                $sid = (string)$serviceId;
192                $src = $requestSource[$sid] ?? null;
193                if (!$src) {
194                    return new Process();
195                }
196                $count = (int)($serviceCounts[$index] ?? 1);
197                for ($i = 0; $i < $count; $i++) {
198                    $requests[] = ['id' => $serviceId, 'source' => $src];
199                }
200            }
201
202            $processEntity = new Process();
203            $processEntity->appointments = $appointmentProcess->appointments ?? [];
204            $processEntity->authKey = $appointmentProcess->authKey ?? null;
205            $processEntity->clients = $appointmentProcess->clients ?? [];
206            $processEntity->scope = $appointmentProcess->scope ?? null;
207            $processEntity->requests = $requests;
208            $processEntity->lastChange = $appointmentProcess->lastChange ?? time();
209            $processEntity->createIP = ClientIpHelper::getClientIp();
210            $processEntity->createTimestamp = time();
211            if (isset($appointmentProcess->queue)) {
212                $processEntity->queue = $appointmentProcess->queue;
213            }
214
215            $result = \App::$http->readPostResult('/process/status/reserved/', $processEntity);
216            $entity = $result?->getEntity();
217            return $entity instanceof Process ? $entity : new Process();
218        } catch (\Exception $e) {
219            ExceptionService::handleException($e);
220        }
221    }
222
223    public static function submitClientData(Process $process): Process
224    {
225        try {
226            $url = "/process/{$process->id}/{$process->authKey}/";
227            $result = \App::$http->readPostResult($url, $process);
228            $entity = $result?->getEntity();
229            if (!$entity instanceof Process) {
230                return new Process();
231            }
232            return $entity;
233        } catch (\Exception $e) {
234            ExceptionService::handleException($e);
235        }
236    }
237
238    public static function preconfirmProcess(Process $process): Process
239    {
240        try {
241            $url = '/process/status/preconfirmed/';
242            $result = \App::$http->readPostResult($url, $process);
243            $entity = $result?->getEntity();
244            if (!$entity instanceof Process) {
245                return new Process();
246            }
247            return $entity;
248        } catch (\Exception $e) {
249            ExceptionService::handleException($e);
250        }
251    }
252
253    public static function confirmProcess(Process $process): Process
254    {
255        try {
256            $url = '/process/status/confirmed/';
257            $result = \App::$http->readPostResult($url, $process);
258            $entity = $result?->getEntity();
259            if (!$entity instanceof Process) {
260                return new Process();
261            }
262            return $entity;
263        } catch (\Exception $e) {
264            ExceptionService::handleException($e);
265        }
266    }
267
268    public static function cancelAppointment(Process $process): Process
269    {
270        try {
271            $url = "/process/{$process->id}/{$process->authKey}/";
272            $result = \App::$http->readDeleteResult($url, []);
273            $entity = $result?->getEntity();
274            if (!$entity instanceof Process) {
275                return new Process();
276            }
277            return $entity;
278        } catch (\Exception $e) {
279            ExceptionService::handleException($e);
280        }
281    }
282
283    public static function sendConfirmationEmail(Process $process): Process
284    {
285        try {
286            $url = "/process/{$process->id}/{$process->authKey}/confirmation/mail/";
287            $result = \App::$http->readPostResult($url, $process);
288            $entity = $result?->getEntity();
289            if (!$entity instanceof Process) {
290                return new Process();
291            }
292            return $entity;
293        } catch (\Exception $e) {
294            ExceptionService::handleException($e);
295        }
296    }
297
298    public static function sendPreconfirmationEmail(Process $process): Process
299    {
300        try {
301            $url = "/process/{$process->id}/{$process->authKey}/preconfirmation/mail/";
302            $result = \App::$http->readPostResult($url, $process);
303            $entity = $result?->getEntity();
304            if (!$entity instanceof Process) {
305                return new Process();
306            }
307            return $entity;
308        } catch (\Exception $e) {
309            ExceptionService::handleException($e);
310        }
311    }
312
313    public static function sendCancellationEmail(Process $process): Process
314    {
315        try {
316            $url = "/process/{$process->id}/{$process->authKey}/delete/mail/";
317            $result = \App::$http->readPostResult($url, $process);
318            $entity = $result?->getEntity();
319            if (!$entity instanceof Process) {
320                return new Process();
321            }
322            return $entity;
323        } catch (\Exception $e) {
324            ExceptionService::handleException($e);
325        }
326    }
327
328    public static function getProcessById(int $processId, string $authKey): Process
329    {
330        try {
331            $resolveReferences = 2;
332            $result = \App::$http->readGetResult("/process/{$processId}/{$authKey}/", [
333                'resolveReferences' => $resolveReferences
334            ]);
335            $entity = $result?->getEntity();
336            if (!$entity instanceof Process) {
337                return new Process();
338            }
339            return $entity;
340        } catch (\Exception $e) {
341            ExceptionService::handleException($e);
342        }
343    }
344
345    public static function getScopesByProviderId(string $source, string|int $providerId): ScopeList
346    {
347        try {
348            $scopeList = self::getScopes();
349            if (!$scopeList instanceof ScopeList) {
350                return new ScopeList();
351            }
352            $result = $scopeList->withProviderID($source, (string)$providerId);
353            if (!$result instanceof ScopeList) {
354                return new ScopeList();
355            }
356            return $result;
357        } catch (\Exception $e) {
358            ExceptionService::handleException($e);
359        }
360    }
361
362    private static function fetchSourceDataFor(string $sourceName): Source
363    {
364        $cacheKey = 'source_' . $sourceName;
365        if (\App::$cache && ($data = \App::$cache->get($cacheKey))) {
366            return $data;
367        }
368
369        $result = \App::$http->readGetResult('/source/' . $sourceName . '/', [
370            'resolveReferences' => 2,
371        ]);
372        $entity = $result?->getEntity();
373        if (!$entity instanceof Source) {
374            return new Source();
375        }
376
377        if (\App::$cache) {
378            \App::$cache->set($cacheKey, $entity, \App::$SOURCE_CACHE_TTL);
379            LoggerService::logInfo('Cache set', [
380                'key' => $cacheKey,
381                'ttl' => \App::$SOURCE_CACHE_TTL,
382                'entity_type' => get_class($entity)
383            ]);
384        }
385
386        return $entity;
387    }
388
389    /**
390     * Akzeptiert sowohl:
391     * - String: "dldb", "dldb,zms", "dldb; zms", "dldb zms", "dldb|zms"
392     * - Array:  ["dldb","zms"]
393     */
394    private static function getSourceNames(): array
395    {
396        $raw = \App::$source_name ?? 'dldb';
397
398        if (is_array($raw)) {
399            $names = array_values(array_filter(array_map('strval', $raw)));
400        } else {
401            $s = (string)$raw;
402            $names = preg_split('/[,\;\|\s]+/', $s, -1, PREG_SPLIT_NO_EMPTY) ?: [];
403        }
404
405        $out = [];
406        foreach ($names as $n) {
407            $n = trim($n);
408            if ($n !== '' && !in_array($n, $out, true)) {
409                $out[] = $n;
410            }
411        }
412
413        return $out ?: ['dldb'];
414    }
415}