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