Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.90% covered (warning)
89.90%
365 / 406
27.78% covered (danger)
27.78%
5 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
MapperService
89.90% covered (warning)
89.90%
365 / 406
27.78% covered (danger)
27.78%
5 / 18
245.44
0.00% covered (danger)
0.00%
0 / 1
 resolveAllowDisabledServicesMix
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 resolveSharedBookingOfficeIds
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 mapScopeForProvider
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
10.24
 extractReservationDuration
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 extractActivationDuration
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
7.29
 mapOfficesWithScope
91.80% covered (success)
91.80%
56 / 61
0.00% covered (danger)
0.00%
0 / 1
50.32
 mapCombinable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 mapServicesWithCombinations
97.37% covered (success)
97.37%
37 / 38
0.00% covered (danger)
0.00%
0 / 1
15
 mapRelations
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 scopeToThinnedScope
96.97% covered (success)
96.97%
64 / 66
0.00% covered (danger)
0.00%
0 / 1
26
 processToThinnedProcess
81.82% covered (warning)
81.82%
36 / 44
0.00% covered (danger)
0.00%
0 / 1
42.36
 thinnedProcessToProcess
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
7.01
 createScope
86.67% covered (warning)
86.67%
39 / 45
0.00% covered (danger)
0.00%
0 / 1
8.15
 createRequests
59.09% covered (warning)
59.09%
13 / 22
0.00% covered (danger)
0.00%
0 / 1
5.10
 contactToThinnedContact
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
 providerToThinnedProvider
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
10
 contactCoordinate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 generateIcsContent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Services\Core;
6
7use BO\Zmsentities\Helper\ProcessPlainText;
8use BO\Zmscitizenapi\Models\Office;
9use BO\Zmscitizenapi\Models\Combinable;
10use BO\Zmscitizenapi\Models\OfficeServiceRelation;
11use BO\Zmscitizenapi\Models\Service;
12use BO\Zmscitizenapi\Models\ThinnedContact;
13use BO\Zmscitizenapi\Models\ThinnedProcess;
14use BO\Zmscitizenapi\Models\ThinnedProvider;
15use BO\Zmscitizenapi\Models\ThinnedScope;
16use BO\Zmscitizenapi\Models\Collections\OfficeList;
17use BO\Zmscitizenapi\Models\Collections\OfficeServiceRelationList;
18use BO\Zmscitizenapi\Models\Collections\ServiceList;
19use BO\Zmscitizenapi\Models\Collections\ThinnedScopeList;
20use BO\Zmscitizenapi\Utils\ClientIpHelper;
21use BO\Zmsentities\Appointment;
22use BO\Zmsentities\Client;
23use BO\Zmsentities\Contact;
24use BO\Zmsentities\Process;
25use BO\Zmsentities\Provider;
26use BO\Zmsentities\Queue;
27use BO\Zmsentities\Request;
28use BO\Zmsentities\Scope;
29use BO\Zmsentities\Collection\ProviderList;
30use BO\Zmsentities\Collection\RequestList;
31use BO\Zmsentities\Collection\RequestRelationList;
32
33/**
34 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
35 * @TODO: Extract class has ExcessiveClassComplexity 101 vs 100
36 */
37class MapperService
38{
39    private static function resolveAllowDisabledServicesMix(\BO\Zmsentities\Schema\Entity $provider): ?array
40    {
41        if (!isset($provider->data['allowDisabledServicesMix']) || !is_array($provider->data['allowDisabledServicesMix'])) {
42            return null;
43        }
44        return array_map('intval', $provider->data['allowDisabledServicesMix']);
45    }
46
47    private static function resolveSharedBookingOfficeIds(\BO\Zmsentities\Schema\Entity $provider): ?array
48    {
49        if (!isset($provider->data['sharedBookingOfficeIds']) || !is_array($provider->data['sharedBookingOfficeIds'])) {
50            return null;
51        }
52        return array_map('intval', $provider->data['sharedBookingOfficeIds']);
53    }
54
55    public static function mapScopeForProvider(
56        int $providerId,
57        ThinnedScopeList $scopes,
58        ?string $providerSource = null
59    ): ?ThinnedScope {
60        foreach ($scopes->getScopes() as $scope) {
61            if (!$scope instanceof ThinnedScope) {
62                continue;
63            }
64
65            $prov = $scope->provider ?? null;
66            if (!$prov) {
67                continue;
68            }
69
70            $provId  = is_object($prov) ? ($prov->id   ?? null) : ($prov['id']    ?? null);
71            $provSrc = is_object($prov) ? ($prov->source ?? null) : ($prov['source'] ?? null);
72
73            if ((string)$provId !== (string)$providerId) {
74                continue;
75            }
76
77            if ($providerSource === null || $providerSource === '') {
78                return $scope;
79            }
80
81            if ((string)$provSrc === (string)$providerSource) {
82                return $scope;
83            }
84        }
85
86        return null;
87    }
88
89    public static function extractReservationDuration(Scope|ThinnedScope|null $scope): ?int
90    {
91        if ($scope === null) {
92            return null;
93        }
94        if ($scope instanceof ThinnedScope) {
95            $reservationDuration = $scope->getReservationDuration();
96            return $reservationDuration !== null ? (int) $reservationDuration : null;
97        }
98        $reservationDuration = $scope->toProperty()?->preferences?->appointment?->reservationDuration?->get();
99        return $reservationDuration !== null ? (int) $reservationDuration : null;
100    }
101
102    public static function extractActivationDuration(Scope|ThinnedScope|null $scope): ?int
103    {
104        if ($scope === null) {
105            return null;
106        }
107
108        if ($scope instanceof ThinnedScope) {
109            $activationDuration = $scope->getActivationDuration();
110            if ($activationDuration === null || $activationDuration === '') {
111                return null;
112            }
113            return (int) $activationDuration;
114        }
115
116        $activationDuration = $scope->toProperty()?->preferences?->appointment?->activationDuration?->get();
117        if ($activationDuration === null || $activationDuration === '') {
118            return null;
119        }
120        return (int) $activationDuration;
121    }
122
123    /**
124     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
125     * @SuppressWarnings(PHPMD.NPathComplexity)
126     * @TODO: Extract mapping logic into specialized mapper classes for each entity type
127     *
128     */
129    public static function mapOfficesWithScope(ProviderList $providerList, bool $showUnpublished = false): OfficeList
130    {
131        $offices = [];
132        $scopes = ZmsApiFacadeService::getScopes();
133        if (!$scopes instanceof ThinnedScopeList) {
134            return new OfficeList();
135        }
136
137        foreach ($providerList as $provider) {
138            // âœ… Source normalisieren: leerer String -> Fallback auf App::$source_name
139            $providerSource = isset($provider->source) && $provider->source !== ''
140                ? (string)$provider->source
141                : (string)\App::$source_name;
142
143            $providerScope = self::mapScopeForProvider(
144                (int) $provider->id,
145                $scopes,
146                $providerSource
147            );
148
149            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
150                continue;
151            }
152
153            $offices[] = new Office(
154                id: isset($provider->id) ? (int) $provider->id : 0,
155                name: isset($provider->displayName) ? $provider->displayName : (isset($provider->name) ? $provider->name : null),
156                address: isset($provider->data['address']) ? $provider->data['address'] : null,
157                showAlternativeLocations: isset($provider->data['showAlternativeLocations']) ? $provider->data['showAlternativeLocations'] : null,
158                displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
159                organization: $provider->data['organization'] ?? null,
160                organizationUnit: $provider->data['organizationUnit'] ?? null,
161                slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
162                geo: isset($provider->data['geo']) ? $provider->data['geo'] : null,
163                disabledByServices: isset($provider->data['dontShowByServices']) ? $provider->data['dontShowByServices'] : [],
164                priority: isset($provider->data['prio']) ? $provider->data['prio'] : 1,
165                scope: isset($providerScope) && !isset($providerScope['errors']) ? new ThinnedScope(
166                    id: isset($providerScope->id) ? (int) $providerScope->id : 0,
167                    provider: isset($providerScope->provider) ? $providerScope->provider : null,
168                    shortName: isset($providerScope->shortName) ? (string) $providerScope->shortName : null,
169                    emailFrom: isset($providerScope->emailFrom) ? (string) $providerScope->emailFrom : null,
170                    emailRequired: isset($providerScope->emailRequired) ? (bool) $providerScope->emailRequired : null,
171                    telephoneActivated: isset($providerScope->telephoneActivated) ? (bool) $providerScope->telephoneActivated : null,
172                    telephoneRequired: isset($providerScope->telephoneRequired) ? (bool) $providerScope->telephoneRequired : null,
173                    customTextfieldActivated: isset($providerScope->customTextfieldActivated) ? (bool) $providerScope->customTextfieldActivated : null,
174                    customTextfieldRequired: isset($providerScope->customTextfieldRequired) ? (bool) $providerScope->customTextfieldRequired : null,
175                    customTextfieldLabel: isset($providerScope->customTextfieldLabel) ? (string) $providerScope->customTextfieldLabel : null,
176                    customTextfield2Activated: isset($providerScope->customTextfield2Activated) ? (bool) $providerScope->customTextfield2Activated : null,
177                    customTextfield2Required: isset($providerScope->customTextfield2Required) ? (bool) $providerScope->customTextfield2Required : null,
178                    customTextfield2Label: isset($providerScope->customTextfield2Label) ? (string) $providerScope->customTextfield2Label : null,
179                    captchaActivatedRequired: isset($providerScope->captchaActivatedRequired) ? (bool) $providerScope->captchaActivatedRequired : null,
180                    infoForAppointment: isset($providerScope->infoForAppointment)
181                        ? ((string) $providerScope->infoForAppointment === '' ? null : (string) $providerScope->infoForAppointment)
182                        : null,
183                    infoForAllAppointments: isset($providerScope->infoForAllAppointments)
184                        ? ((string) $providerScope->infoForAllAppointments === '' ? null : (string) $providerScope->infoForAllAppointments)
185                        : null,
186                    appointmentsPerMail: isset($providerScope->appointmentsPerMail) ? ((string) $providerScope->appointmentsPerMail === '' ? null : (string) $providerScope->appointmentsPerMail) : null,
187                    slotsPerAppointment: isset($providerScope->slotsPerAppointment) ? ((string) $providerScope->slotsPerAppointment === '' ? null : (string) $providerScope->slotsPerAppointment) : null,
188                    whitelistedMails: isset($providerScope->whitelistedMails) ? ((string) $providerScope->whitelistedMails === '' ? null : (string) $providerScope->whitelistedMails) : null,
189                    reservationDuration: (int) self::extractReservationDuration($providerScope),
190                    activationDuration: self::extractActivationDuration($providerScope),
191                    hint: isset($providerScope->hint) ? (trim((string) $providerScope->hint) === '' ? null : (string) $providerScope->hint) : null
192                ) : null,
193                slotsPerAppointment: isset($providerScope) && !isset($providerScope['errors']) && isset($providerScope->slotsPerAppointment) ? ((string) $providerScope->slotsPerAppointment === '' ? null : (string) $providerScope->slotsPerAppointment) : null,
194                parentId: isset($provider->parent_id) ? (int) $provider->parent_id : null,
195                allowDisabledServicesMix: self::resolveAllowDisabledServicesMix($provider),
196                sharedBookingOfficeIds: self::resolveSharedBookingOfficeIds($provider)
197            );
198        }
199
200        return new OfficeList($offices);
201    }
202
203    public static function mapCombinable(array $serviceCombinations): ?Combinable
204    {
205        return !empty($serviceCombinations) ? new Combinable($serviceCombinations) : null;
206    }
207
208    /**
209     * Map services with combinations based on request and relation lists.
210     *
211     * @SuppressWarnings(PHPMD.NPathComplexity)
212     */
213    public static function mapServicesWithCombinations(
214        RequestList $requestList,
215        RequestRelationList $relationList,
216        bool $showUnpublished = false
217    ): ServiceList {
218        /** @var array<string, array<int>> $servicesProviderIds */
219        $servicesProviderIds = [];
220        foreach ($relationList as $relation) {
221            if (!$showUnpublished && !$relation->isPublic()) {
222                continue;
223            }
224
225            $serviceId = $relation->request->id;
226            $servicesProviderIds[$serviceId] ??= [];
227            $servicesProviderIds[$serviceId][] = $relation->provider->id;
228        }
229
230        $services = [];
231        $requestArray = iterator_to_array($requestList);
232        usort($requestArray, function ($a, $b) {
233
234            return $a->getId() <=> $b->getId();
235            // Sorting by service ID (ascending order)
236        });
237        foreach ($requestArray as $service) {
238            if (
239                !$showUnpublished
240                && isset($service->getAdditionalData()['public'])
241                && !$service->getAdditionalData()['public']
242            ) {
243                continue;
244            }
245
246            /** @var array<string, array<int>> $serviceCombinations */
247            $serviceCombinations = [];
248            $combinableData = $service->getAdditionalData()['combinable'] ?? [];
249            foreach ($combinableData as $combinationServiceId) {
250                $commonProviders = array_intersect($servicesProviderIds[$service->getId()] ?? [], $servicesProviderIds[$combinationServiceId] ?? []);
251                $serviceCombinations[$combinationServiceId] = !empty($commonProviders) ? array_values($commonProviders) : [];
252            }
253
254            $combinable = self::mapCombinable($serviceCombinations);
255
256            $extra = $service->getAdditionalData() ?? [];
257            $parentId  = isset($service->parent_id)  ? (int)$service->parent_id  : (isset($extra['parent_id'])  ? (int)$extra['parent_id']  : null);
258            $variantId = isset($service->variant_id) ? (int)$service->variant_id : (isset($extra['variant_id']) ? (int)$extra['variant_id'] : null);
259
260            if (!empty($servicesProviderIds[$service->getId()])) {
261                $services[] = new Service(
262                    id: (int) $service->getId(),
263                    name: $service->getName(),
264                    maxQuantity: $service->getAdditionalData()['maxQuantity'] ?? 1,
265                    combinable: $combinable ?? new Combinable(),
266                    parentId: $parentId,
267                    variantId: $variantId,
268                    showOnStartPage: $service->getAdditionalData()['showOnStartPage'] ?? true,
269                    rootParentId: (int) $service->getRootParentId(),
270                );
271            }
272        }
273
274        return new ServiceList($services);
275    }
276
277
278    public static function mapRelations(
279        RequestRelationList $relationList,
280        bool $showUnpublished = false
281    ): OfficeServiceRelationList {
282        $relations = [];
283        foreach ($relationList as $relation) {
284            if (!$showUnpublished && !$relation->isPublic()) {
285                continue;
286            }
287
288            $relations[] = new OfficeServiceRelation(
289                officeId: (int) $relation->provider->id,
290                serviceId: (int) $relation->request->id,
291                slots: intval($relation->slots),
292                public: (bool) $relation->isPublic(),
293                maxQuantity: (int) $relation->maxQuantity
294            );
295        }
296
297        return new OfficeServiceRelationList($relations);
298    }
299
300    /**
301     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
302     * @SuppressWarnings(PHPMD.NPathComplexity)
303     */
304    public static function scopeToThinnedScope(Scope $scope): ThinnedScope
305    {
306        if (!$scope || !isset($scope->id)) {
307            return new ThinnedScope();
308        }
309
310        $thinnedProvider = null;
311        try {
312            $provider = null;
313            if (isset($scope->provider) && $scope->provider instanceof Provider) {
314                $provider = $scope->provider;
315            } elseif (isset($scope->provider) || method_exists($scope, 'getProvider')) {
316                $provider = $scope->getProvider();
317            }
318            if ($provider instanceof Provider) {
319                $thinnedProvider = self::providerToThinnedProvider($provider);
320            }
321        } catch (\BO\Zmsentities\Exception\ScopeMissingProvider $e) {
322            $thinnedProvider = null;
323        }
324
325        return new ThinnedScope(
326            id: (int) ($scope->id ?? 0),
327            provider: $thinnedProvider,
328            shortName: isset($scope->shortName) ? (string) $scope->shortName : null,
329            emailFrom: (string) $scope->getEmailFrom(),
330            emailRequired: $scope->getEmailRequired() === null
331                ? null
332                : (bool) $scope->getEmailRequired(),
333            telephoneActivated: $scope->getTelephoneActivated() === null
334                ? null
335                : (bool) $scope->getTelephoneActivated(),
336            telephoneRequired: $scope->getTelephoneRequired() === null
337                ? null
338                : (bool) $scope->getTelephoneRequired(),
339            customTextfieldActivated: $scope->getCustomTextfieldActivated() === null
340                ? null
341                : (bool) $scope->getCustomTextfieldActivated(),
342            customTextfieldRequired: $scope->getCustomTextfieldRequired() === null
343                ? null
344                : (bool) $scope->getCustomTextfieldRequired(),
345            customTextfieldLabel: $scope->getCustomTextfieldLabel() === null
346                ? null
347                : (string) $scope->getCustomTextfieldLabel(),
348            customTextfield2Activated: $scope->getCustomTextfield2Activated() === null
349                ? null
350                : (bool) $scope->getCustomTextfield2Activated(),
351            customTextfield2Required: $scope->getCustomTextfield2Required() === null
352                ? null
353                : (bool) $scope->getCustomTextfield2Required(),
354            customTextfield2Label: $scope->getCustomTextfield2Label() === null
355                ? null
356                : (string) $scope->getCustomTextfield2Label(),
357            captchaActivatedRequired: $scope->getCaptchaActivatedRequired() === null
358                ? null
359                : (bool) $scope->getCaptchaActivatedRequired(),
360            infoForAppointment: $scope->getInfoForAppointment() === null
361                ? null
362                : (string) $scope->getInfoForAppointment(),
363            infoForAllAppointments: $scope->getInfoForAllAppointments() === null
364                ? null
365                : (string) $scope->getInfoForAllAppointments(),
366            slotsPerAppointment: $scope->getSlotsPerAppointment() === null
367                ? null
368                : (string) $scope->getSlotsPerAppointment(),
369            appointmentsPerMail: $scope->getAppointmentsPerMail() !== null
370                ? (string) $scope->getAppointmentsPerMail()
371                : null,
372            whitelistedMails: $scope->getWhitelistedMails() === null
373                ? null
374                : (string) $scope->getWhitelistedMails(),
375            reservationDuration: MapperService::extractReservationDuration($scope),
376            activationDuration: MapperService::extractActivationDuration($scope),
377            hint: (trim((string) ($scope->getScopeHint() ?? '')) === '') ? null : (string) $scope->getScopeHint()
378        );
379    }
380
381    /**
382     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
383     * @SuppressWarnings(PHPMD.NPathComplexity)
384     * @TODO: Break down process mapping into smaller, focused methods
385     */
386    public static function processToThinnedProcess(Process $myProcess): ThinnedProcess
387    {
388        if (!$myProcess || !isset($myProcess->id)) {
389            return new ThinnedProcess();
390        }
391
392        $subRequestCounts = [];
393        $mainServiceId = null;
394        $mainServiceName = null;
395        $mainServiceCount = 0;
396        $requests = iterator_to_array($myProcess->getRequests());
397        if (count($requests) > 0) {
398            $mainServiceId = $requests[0]->id;
399            foreach ($requests as $request) {
400                if ($request->id === $mainServiceId) {
401                    $mainServiceCount++;
402                    if (!$mainServiceName && isset($request->name)) {
403                        $mainServiceName = $request->name;
404                    }
405                } else {
406                    if (!isset($subRequestCounts[$request->id])) {
407                        $subRequestCounts[$request->id] = [
408                            'id' => (int) $request->id,
409                            'name'  => $request->name,
410                            'count' => 0,
411                        ];
412                    }
413                    $subRequestCounts[$request->id]['count']++;
414                }
415            }
416        }
417
418        // Generate ICS content if process has appointments with time
419        $icsContent = self::generateIcsContent($myProcess);
420
421        return new ThinnedProcess(
422            processId: isset($myProcess->id) ? (int) $myProcess->id : 0,
423            timestamp: (isset($myProcess->appointments[0]) && isset($myProcess->appointments[0]->date)) ? strval($myProcess->appointments[0]->date) : null,
424            authKey: isset($myProcess->authKey) ? $myProcess->authKey : null,
425            captchaToken: isset($myProcess->captchaToken) ? $myProcess->captchaToken : null,
426            familyName: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->familyName)) ? $myProcess->clients[0]->familyName : null,
427            customTextfield: isset($myProcess->customTextfield) ? $myProcess->customTextfield : null,
428            customTextfield2: isset($myProcess->customTextfield2) ? $myProcess->customTextfield2 : null,
429            email: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->email)) ? $myProcess->clients[0]->email : null,
430            telephone: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->telephone)) ? $myProcess->clients[0]->telephone : null,
431            officeName: (isset($myProcess->scope->contact) && isset($myProcess->scope->contact->name)) ? $myProcess->scope->contact->name : null,
432            officeId: (isset($myProcess->scope->provider) && isset($myProcess->scope->provider->id)) ? (int) $myProcess->scope->provider->id : 0,
433            scope: isset($myProcess->scope) ? self::scopeToThinnedScope($myProcess->scope) : null,
434            subRequestCounts: array_values($subRequestCounts),
435            serviceId: isset($mainServiceId) ? (int) $mainServiceId : 0,
436            serviceName: isset($mainServiceName) ? $mainServiceName : null,
437            serviceCount: $mainServiceCount,
438            status: (isset($myProcess->queue) && isset($myProcess->queue->status)) ? $myProcess->queue->status : null,
439            slotCount: (isset($myProcess->appointments[0]) && isset($myProcess->appointments[0]->slotCount)) ? (int) $myProcess->appointments[0]->slotCount : null,
440            displayNumber: ($myProcess->getDisplayNumber() ?: null),
441            icsContent: isset($icsContent) ? $icsContent : null
442        );
443    }
444
445    public static function thinnedProcessToProcess(ThinnedProcess $thinnedProcess): Process
446    {
447        if (!$thinnedProcess || !isset($thinnedProcess->processId)) {
448            return new Process();
449        }
450
451        $processEntity = new Process();
452        $processEntity->id = $thinnedProcess->processId;
453        $processEntity->authKey = $thinnedProcess->authKey ?? null;
454        $customTextfield = $thinnedProcess->customTextfield ?? null;
455        $processEntity->customTextfield = $customTextfield === null ? null : ProcessPlainText::normalize($customTextfield);
456        $customTextfield2 = $thinnedProcess->customTextfield2 ?? null;
457        $processEntity->customTextfield2 = $customTextfield2 === null
458            ? null
459            : ProcessPlainText::normalize($customTextfield2);
460        $processEntity->captchaToken = $thinnedProcess->captchaToken ?? null;
461
462        $client = new Client();
463        $client->familyName = $thinnedProcess->familyName ?? null;
464        $client->email = $thinnedProcess->email ?? null;
465        $client->telephone = $thinnedProcess->telephone ?? null;
466        $processEntity->clients = [$client];
467
468        $appointment = new Appointment();
469        $appointment->slotCount = $thinnedProcess->slotCount ?? null;
470        $appointment->date = $thinnedProcess->timestamp ?? null;
471        $processEntity->appointments = [$appointment];
472        $processEntity->scope = self::createScope($thinnedProcess);
473        $processEntity->requests = self::createRequests($thinnedProcess);
474
475        if (isset($thinnedProcess->status)) {
476            $processEntity->queue = new Queue(['status' => $thinnedProcess->status]);
477            $processEntity->status = $thinnedProcess->status;
478        }
479
480        if (isset($thinnedProcess->displayNumber)) {
481            $processEntity->displayNumber = $thinnedProcess->displayNumber;
482        }
483
484        $processEntity->lastChange = time();
485        $processEntity->createIP = ClientIpHelper::getClientIp();
486        $processEntity->createTimestamp = time();
487        return $processEntity;
488    }
489
490    private static function createScope(ThinnedProcess $thinnedProcess): Scope
491    {
492        $scope = new Scope();
493        if ($thinnedProcess->scope) {
494            $providerSource = $thinnedProcess->scope->provider->source ?? 'dldb';
495
496            $scope->id = $thinnedProcess->scope->id;
497            $scope->source = $providerSource;
498
499            $scope->preferences = [
500                'client' => [
501                    'appointmentsPerMail' => $thinnedProcess->scope->getAppointmentsPerMail() ?? null,
502                    'slotsPerAppointment' => $thinnedProcess->scope->getSlotsPerAppointment() ?? null,
503                    "whitelistedMails" => $thinnedProcess->scope->getWhitelistedMails() ?? null,
504                    'emailFrom' => $thinnedProcess->scope->getEmailFrom() ?? null,
505                    'emailRequired' => $thinnedProcess->scope->getEmailRequired() ?? false,
506                    'telephoneActivated' => $thinnedProcess->scope->getTelephoneActivated() ?? false,
507                    'telephoneRequired' => $thinnedProcess->scope->getTelephoneRequired() ?? false,
508                    'customTextfieldActivated' => $thinnedProcess->scope->getCustomTextfieldActivated() ?? false,
509                    'customTextfieldRequired' => $thinnedProcess->scope->getCustomTextfieldRequired() ?? false,
510                    'customTextfieldLabel' => $thinnedProcess->scope->getCustomTextfieldLabel() ?? null,
511                    'customTextfield2Activated' => $thinnedProcess->scope->getCustomTextfield2Activated() ?? false,
512                    'customTextfield2Required' => $thinnedProcess->scope->getCustomTextfield2Required() ?? false,
513                    'customTextfield2Label' => $thinnedProcess->scope->getCustomTextfield2Label() ?? null
514                ]
515            ];
516        }
517        if (isset($thinnedProcess->officeName)) {
518            $scope->contact = new Contact();
519            $scope->contact->name = $thinnedProcess->officeName;
520        }
521        if (isset($thinnedProcess->officeId)) {
522            $scope->provider = new Provider();
523            $scope->provider->id = $thinnedProcess->officeId;
524            if (isset($thinnedProcess->scope->provider)) {
525                $provider = $thinnedProcess->scope->provider;
526                $scope->provider->name  = $provider->name ?? null;
527                $scope->provider->displayName = $provider->displayName ?? null;
528
529                if (isset($provider->contact)) {
530                    $scope->provider->contact = new Contact();
531                    $scope->provider->contact->street = $provider->contact->street ?? null;
532                    $scope->provider->contact->streetNumber = $provider->contact->streetNumber ?? null;
533                }
534                if ($provider->lat !== null || $provider->lon !== null) {
535                    $scope->provider->data = [
536                        'geo' => [
537                            'lat' => $provider->lat,
538                            'lon' => $provider->lon,
539                        ],
540                    ];
541                }
542            }
543
544            $scope->provider->source = $thinnedProcess->scope->provider->source ?? null;
545        }
546
547        return $scope;
548    }
549
550    private static function createRequests(ThinnedProcess $thinnedProcess): array
551    {
552        $providerSource = $thinnedProcess->scope->provider->source ?? 'dldb';
553
554        $requests = [];
555        $mainServiceId = $thinnedProcess->serviceId ?? null;
556        $mainServiceName = $thinnedProcess->serviceName ?? null;
557        $mainServiceCount = $thinnedProcess->serviceCount ?? 0;
558
559        for ($i = 0; $i < $mainServiceCount; $i++) {
560            $request = new Request();
561            $request->id = $mainServiceId;
562            $request->name = $mainServiceName;
563            $request->source = $providerSource;
564            $requests[] = $request;
565        }
566
567        foreach ($thinnedProcess->subRequestCounts ?? [] as $subRequest) {
568            $subId = $subRequest['id'] ?? null;
569            $subName = $subRequest['name'] ?? null;
570            $count = (int)($subRequest['count'] ?? 0);
571
572            for ($i = 0; $i < $count; $i++) {
573                $request = new Request();
574                $request->id = $subId;
575                $request->name = $subName;
576                $request->source = $providerSource;
577                $requests[] = $request;
578            }
579        }
580
581        return $requests;
582    }
583
584    public static function contactToThinnedContact($contact): ThinnedContact
585    {
586        if (is_array($contact)) {
587            return new ThinnedContact(
588                city: $contact['city'] ?? null,
589                country: $contact['country'] ?? null,
590                name: $contact['name'] ?? null,
591                postalCode: isset($contact['postalCode']) ? (is_null($contact['postalCode']) ? null : (string)$contact['postalCode']) : null,
592                region: $contact['region'] ?? null,
593                street: $contact['street'] ?? null,
594                streetNumber: $contact['streetNumber'] ?? null
595            );
596        }
597
598        return new ThinnedContact(
599            city: $contact->city ?? null,
600            country: $contact->country ?? null,
601            name: $contact->name ?? null,
602            postalCode: isset($contact->postalCode) ? (is_null($contact->postalCode) ? null : (string)$contact->postalCode) : null,
603            region: $contact->region ?? null,
604            street: $contact->street ?? null,
605            streetNumber: $contact->streetNumber ?? null
606        );
607    }
608
609    public static function providerToThinnedProvider(Provider $provider): ThinnedProvider
610    {
611        $geoLat = $provider->data['geo']['lat'] ?? null;
612        $geoLon = $provider->data['geo']['lon'] ?? null;
613        $contactLat = self::contactCoordinate($provider->contact ?? null, 'lat');
614        $contactLon = self::contactCoordinate($provider->contact ?? null, 'lon');
615
616        return new ThinnedProvider(
617            id: isset($provider->id) ? (int) $provider->id : null,
618            name: isset($provider->name) ? $provider->name : null,
619            displayName: isset($provider->displayName) ? $provider->displayName : null,
620            source: isset($provider->source) ? $provider->source : null,
621            lon: isset($geoLon) ? (float) $geoLon : (isset($contactLon) ? (float) $contactLon : null),
622            lat: isset($geoLat) ? (float) $geoLat : (isset($contactLat) ? (float) $contactLat : null),
623            contact: isset($provider->contact) ? self::contactToThinnedContact($provider->contact) : null
624        );
625    }
626
627    /**
628     * Read lat/lon from contact whether it is stored as array or object (see contact.json).
629     */
630    private static function contactCoordinate(mixed $contact, string $key): mixed
631    {
632        if ($contact === null) {
633            return null;
634        }
635        if (is_array($contact)) {
636            return $contact[$key] ?? null;
637        }
638        return $contact->$key ?? null;
639    }
640
641    private static function generateIcsContent(Process $process): ?string
642    {
643        if (!isset($process->appointments[0]) || !$process->appointments[0]->hasTime()) {
644            return null;
645        }
646
647        $status = $process->getStatus();
648        $queueStatus = $process->toProperty()->queue->status->get();
649        if (
650            in_array($status, [Process::STATUS_DELETED, Process::STATUS_BLOCKED], true)
651            || in_array($queueStatus, [Process::STATUS_DELETED, Process::STATUS_BLOCKED], true)
652        ) {
653            return null;
654        }
655
656        $content = ZmsApiClientService::getIcsContent((int) $process->getId(), (string) $process->getAuthKey());
657        return $content ?: null;
658    }
659}