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