Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
190 / 209
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MapperService
90.91% covered (success)
90.91%
190 / 209
40.00% covered (danger)
40.00%
4 / 10
120.26
0.00% covered (danger)
0.00%
0 / 1
 mapScopeForProvider
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 mapOfficesWithScope
94.29% covered (success)
94.29%
33 / 35
0.00% covered (danger)
0.00%
0 / 1
27.14
 mapCombinable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 mapServicesWithCombinations
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
10
 mapRelations
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 scopeToThinnedScope
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
13.38
 processToThinnedProcess
80.56% covered (warning)
80.56%
29 / 36
0.00% covered (danger)
0.00%
0 / 1
38.06
 thinnedProcessToProcess
92.31% covered (success)
92.31%
60 / 65
0.00% covered (danger)
0.00%
0 / 1
10.05
 contactToThinnedContact
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 providerToThinnedProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Services\Core;
6
7use BO\Zmscitizenapi\Helper\ClientIpHelper;
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\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(int $providerId, ?ThinnedScopeList $scopes): ThinnedScope
38    {
39        if (!$scopes) {
40            return new ThinnedScope();
41        }
42
43        $matchingScope = new ThinnedScope();
44        foreach ($scopes->getScopes() as $scope) {
45            if ($scope->provider && $scope->provider->id === $providerId) {
46                $matchingScope = $scope;
47                break;
48            }
49        }
50
51        return $matchingScope;
52    }
53
54    /**
55     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
56     * @SuppressWarnings(PHPMD.NPathComplexity)
57     * @TODO: Extract mapping logic into specialized mapper classes for each entity type
58     */
59    public static function mapOfficesWithScope(ProviderList $providerList, bool $showUnpublished = false): OfficeList
60    {
61        $offices = [];
62        $scopes = ZmsApiFacadeService::getScopes();
63        if (!$scopes instanceof ThinnedScopeList) {
64            return new OfficeList();
65        }
66
67        foreach ($providerList as $provider) {
68            $providerScope = self::mapScopeForProvider((int) $provider->id, $scopes);
69
70            if (!$showUnpublished && isset($provider->data['public']) && !(bool) $provider->data['public']) {
71                continue;
72            }
73
74            $offices[] = new Office(
75                id: isset($provider->id) ? (int) $provider->id : 0,
76                name: isset($provider->displayName) ? $provider->displayName : (isset($provider->name) ? $provider->name : null),
77                address: isset($provider->data['address']) ? $provider->data['address'] : null,
78                showAlternativeLocations: isset($provider->data['showAlternativeLocations']) ? $provider->data['showAlternativeLocations'] : null,
79                displayNameAlternatives: $provider->data['displayNameAlternatives'] ?? [],
80                organization: $provider->data['organization'] ?? null,
81                organizationUnit: $provider->data['organizationUnit'] ?? null,
82                slotTimeInMinutes: $provider->data['slotTimeInMinutes'] ?? null,
83                geo: isset($provider->data['geo']) ? $provider->data['geo'] : null,
84                disabledByServices: isset($provider->data['dontShowByServices']) ? $provider->data['dontShowByServices'] : [],
85                scope: isset($providerScope) && !isset($providerScope['errors']) ? new ThinnedScope(
86                    id: isset($providerScope->id) ? (int) $providerScope->id : 0,
87                    provider: isset($providerScope->provider) ? $providerScope->provider : null,
88                    shortName: isset($providerScope->shortName) ? $providerScope->shortName : null,
89                    emailFrom: isset($providerScope->emailFrom) ? $providerScope->emailFrom : null,
90                    emailRequired: isset($providerScope->emailRequired) ? (bool) $providerScope->emailRequired : null,
91                    telephoneActivated: isset($providerScope->telephoneActivated) ? (bool) $providerScope->telephoneActivated : null,
92                    telephoneRequired: isset($providerScope->telephoneRequired) ? (bool) $providerScope->telephoneRequired : null,
93                    customTextfieldActivated: isset($providerScope->customTextfieldActivated) ? (bool) $providerScope->customTextfieldActivated : null,
94                    customTextfieldRequired: isset($providerScope->customTextfieldRequired) ? (bool) $providerScope->customTextfieldRequired : null,
95                    customTextfieldLabel: isset($providerScope->customTextfieldLabel) ? $providerScope->customTextfieldLabel : null,
96                    captchaActivatedRequired: isset($providerScope->captchaActivatedRequired) ? (bool) $providerScope->captchaActivatedRequired : null,
97                    displayInfo: isset($providerScope->displayInfo) ? $providerScope->displayInfo : null
98                ) : null
99            );
100        }
101
102        return new OfficeList($offices);
103    }
104
105    public static function mapCombinable(array $serviceCombinations): ?Combinable
106    {
107        return !empty($serviceCombinations) ? new Combinable($serviceCombinations) : null;
108    }
109
110    /**
111     * Map services with combinations based on request and relation lists.
112     *
113     * @param RequestList $requestList
114     * @param RequestRelationList $relationList
115     * @return ServiceList
116     */
117    public static function mapServicesWithCombinations(
118        RequestList $requestList,
119        RequestRelationList $relationList,
120        bool $showUnpublished = false
121    ): ServiceList {
122        /** @var array<string, array<int>> $servicesProviderIds */
123        $servicesProviderIds = [];
124        foreach ($relationList as $relation) {
125            if (!$showUnpublished && !$relation->isPublic()) {
126                continue;
127            }
128
129            $serviceId = $relation->request->id;
130            $servicesProviderIds[$serviceId] ??= [];
131            $servicesProviderIds[$serviceId][] = $relation->provider->id;
132        }
133
134        /** @var Service[] $services */
135        $services = [];
136        $requestArray = iterator_to_array($requestList);
137        usort($requestArray, function ($a, $b) {
138
139            return $a->getId() <=> $b->getId();
140            // Sorting by service ID (ascending order)
141        });
142        foreach ($requestArray as $service) {
143            if (
144                !$showUnpublished
145                && isset($service->getAdditionalData()['public'])
146                && !$service->getAdditionalData()['public']
147            ) {
148                continue;
149            }
150
151            /** @var array<string, array<int>> $serviceCombinations */
152            $serviceCombinations = [];
153            $combinableData = $service->getAdditionalData()['combinable'] ?? [];
154            foreach ($combinableData as $combinationServiceId) {
155                $commonProviders = array_intersect($servicesProviderIds[$service->getId()] ?? [], $servicesProviderIds[$combinationServiceId] ?? []);
156                $serviceCombinations[$combinationServiceId] = !empty($commonProviders) ? array_values($commonProviders) : [];
157            }
158
159            $combinable = self::mapCombinable($serviceCombinations);
160            $services[] = new Service(id: (int) $service->getId(), name: $service->getName(), maxQuantity: $service->getAdditionalData()['maxQuantity'] ?? 1, combinable: $combinable ?? new Combinable());
161        }
162
163        return new ServiceList($services);
164    }
165
166
167    public static function mapRelations(
168        RequestRelationList $relationList,
169        bool $showUnpublished = false
170    ): OfficeServiceRelationList {
171        $relations = [];
172        foreach ($relationList as $relation) {
173            if (!$showUnpublished && !$relation->isPublic()) {
174                continue;
175            }
176
177            $relations[] = new OfficeServiceRelation(
178                officeId: (int) $relation->provider->id,
179                serviceId: (int) $relation->request->id,
180                slots: intval($relation->slots),
181                public: (bool) $relation->isPublic(),
182                maxQuantity: (int) $relation->maxQuantity
183            );
184        }
185
186        return new OfficeServiceRelationList($relations);
187    }
188
189    public static function scopeToThinnedScope(Scope $scope): ThinnedScope
190    {
191        if (!$scope || !isset($scope->id)) {
192            return new ThinnedScope();
193        }
194
195        $thinnedProvider = null;
196        try {
197            if (isset($scope->provider)) {
198                $provider = $scope->provider;
199                $contact = $provider->contact ?? null;
200                $thinnedProvider = new ThinnedProvider(id: isset($provider->id) ? (int) $provider->id : null, name: $provider->name ?? null, source: $provider->source ?? null, contact: $contact ? self::contactToThinnedContact($contact) : null);
201            }
202        } catch (\BO\Zmsentities\Exception\ScopeMissingProvider $e) {
203            $thinnedProvider = null;
204        }
205
206        return new ThinnedScope(
207            id: (int) ($scope->id ?? 0),
208            provider: $thinnedProvider,
209            shortName: $scope->shortName ?? null,
210            emailFrom: (string) $scope->getEmailFrom() ?? null,
211            emailRequired: isset($scope->data['emailRequired']) ? (bool) $scope->data['emailRequired'] : null,
212            telephoneActivated: isset($scope->data['telephoneActivated']) ? (bool) $scope->data['telephoneActivated'] : null,
213            telephoneRequired: isset($scope->data['telephoneRequired']) ? (bool) $scope->data['telephoneRequired'] : null,
214            customTextfieldActivated: isset($scope->data['customTextfieldActivated']) ? (bool) $scope->data['customTextfieldActivated'] : null,
215            customTextfieldRequired: isset($scope->data['customTextfieldRequired']) ? (bool) $scope->data['customTextfieldRequired'] : null,
216            customTextfieldLabel: $scope->data['customTextfieldLabel'] ?? null,
217            captchaActivatedRequired: isset($scope->data['captchaActivatedRequired']) ? (bool) $scope->data['captchaActivatedRequired'] : null,
218            displayInfo: $scope->data['displayInfo'] ?? null
219        );
220    }
221
222    /**
223     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
224     * @SuppressWarnings(PHPMD.NPathComplexity)
225     * @TODO: Break down process mapping into smaller, focused methods
226     */
227    public static function processToThinnedProcess(Process $myProcess): ThinnedProcess
228    {
229        if (!$myProcess || !isset($myProcess->id)) {
230            return new ThinnedProcess();
231        }
232
233        $subRequestCounts = [];
234        $mainServiceId = null;
235        $mainServiceCount = 0;
236        $requests = $myProcess->getRequests() ?? [];
237        if ($requests) {
238            $requests = is_array($requests) ? $requests : iterator_to_array($requests);
239            if (count($requests) > 0) {
240                $mainServiceId = $requests[0]->id;
241                foreach ($requests as $request) {
242                    if ($request->id === $mainServiceId) {
243                        $mainServiceCount++;
244                    } else {
245                        if (!isset($subRequestCounts[$request->id])) {
246                            $subRequestCounts[$request->id] = [
247                                'id' => (int) $request->id,
248                                'count' => 0,
249                            ];
250                        }
251                        $subRequestCounts[$request->id]['count']++;
252                    }
253                }
254            }
255        }
256
257        return new ThinnedProcess(
258            processId: isset($myProcess->id) ? (int) $myProcess->id : 0,
259            timestamp: (isset($myProcess->appointments[0]) && isset($myProcess->appointments[0]->date)) ? strval($myProcess->appointments[0]->date) : null,
260            authKey: isset($myProcess->authKey) ? $myProcess->authKey : null,
261            captchaToken: isset($myProcess->captchaToken) ? $myProcess->captchaToken : null,
262            familyName: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->familyName)) ? $myProcess->clients[0]->familyName : null,
263            customTextfield: isset($myProcess->customTextfield) ? $myProcess->customTextfield : null,
264            email: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->email)) ? $myProcess->clients[0]->email : null,
265            telephone: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->telephone)) ? $myProcess->clients[0]->telephone : null,
266            officeName: (isset($myProcess->scope->contact) && isset($myProcess->scope->contact->name)) ? $myProcess->scope->contact->name : null,
267            officeId: (isset($myProcess->scope->provider) && isset($myProcess->scope->provider->id)) ? (int) $myProcess->scope->provider->id : 0,
268            scope: isset($myProcess->scope) ? self::scopeToThinnedScope($myProcess->scope) : null,
269            subRequestCounts: isset($subRequestCounts) ? array_values($subRequestCounts) : [],
270            serviceId: isset($mainServiceId) ? (int) $mainServiceId : 0,
271            serviceCount: isset($mainServiceCount) ? $mainServiceCount : 0,
272            status: (isset($myProcess->queue) && isset($myProcess->queue->status)) ? $myProcess->queue->status : null
273        );
274    }
275
276    public static function thinnedProcessToProcess(ThinnedProcess $thinnedProcess): Process
277    {
278        if (!$thinnedProcess || !isset($thinnedProcess->processId)) {
279            return new Process();
280        }
281
282        $processEntity = new Process();
283        $processEntity->id = $thinnedProcess->processId;
284        $processEntity->authKey = $thinnedProcess->authKey ?? null;
285        $processEntity->customTextfield = $thinnedProcess->customTextfield ?? null;
286        $processEntity->captchaToken = $thinnedProcess->captchaToken ?? null;
287
288        $client = new Client();
289        $client->familyName = $thinnedProcess->familyName ?? null;
290        $client->email = $thinnedProcess->email ?? null;
291        $client->telephone = $thinnedProcess->telephone ?? null;
292        $processEntity->clients = [$client];
293        $appointment = new Appointment();
294        $appointment->date = $thinnedProcess->timestamp ?? null;
295        $processEntity->appointments = [$appointment];
296        // Set scope with all required fields
297        $scope = new Scope();
298        if ($thinnedProcess->scope) {
299            $scope->id = $thinnedProcess->scope->id;
300            $scope->source = \App::$source_name;
301
302            // Set preferences as array structure
303            $scope->preferences = [
304                'client' => [
305                    'emailFrom' => $thinnedProcess->scope->getEmailFrom() ?? null,
306                    'emailRequired' => $thinnedProcess->scope->getEmailRequired() ?? false,
307                    'telephoneActivated' => $thinnedProcess->scope->getTelephoneActivated() ?? false,
308                    'telephoneRequired' => $thinnedProcess->scope->getTelephoneRequired() ?? false,
309                    'customTextfieldActivated' => $thinnedProcess->scope->getCustomTextfieldActivated() ?? false,
310                    'customTextfieldRequired' => $thinnedProcess->scope->getCaptchaActivatedRequired() ?? false,
311                    'customTextfieldLabel' => $thinnedProcess->scope->getCustomTextfieldLabel() ?? null
312                ],
313                'notifications' => [
314                    'enabled' => true
315                ]
316            ];
317        }
318        if (isset($thinnedProcess->officeName)) {
319            $scope->contact = new Contact();
320            $scope->contact->name = $thinnedProcess->officeName;
321        }
322        if (isset($thinnedProcess->officeId)) {
323            $scope->provider = new Provider();
324            $scope->provider->id = $thinnedProcess->officeId;
325            $scope->provider->source = \App::$source_name;
326        }
327        $processEntity->scope = $scope;
328        if (isset($thinnedProcess->status)) {
329            $processEntity->queue = new \stdClass();
330            $processEntity->queue->status = $thinnedProcess->status;
331            $processEntity->status = $thinnedProcess->status;
332        }
333
334        $mainServiceId = $thinnedProcess->serviceId ?? null;
335        $mainServiceCount = $thinnedProcess->serviceCount ?? 0;
336        $subRequestCounts = $thinnedProcess->subRequestCounts ?? [];
337        $requests = [];
338        for ($i = 0; $i < $mainServiceCount; $i++) {
339            $request = new Request();
340            $request->id = $mainServiceId;
341            $request->source = \App::$source_name;
342            $requests[] = $request;
343        }
344        foreach ($subRequestCounts as $subRequest) {
345            for ($i = 0; $i < ($subRequest['count'] ?? 0); $i++) {
346                $request = new Request();
347                $request->id = $subRequest['id'];
348                $request->source = \App::$source_name;
349                $requests[] = $request;
350            }
351        }
352        $processEntity->requests = $requests;
353        $processEntity->lastChange = time();
354        $processEntity->createIP = ClientIpHelper::getClientIp();
355        $processEntity->createTimestamp = time();
356        return $processEntity;
357    }
358
359    /**
360     * Converts a raw or existing contact object/array into a ThinnedContact model.
361     *
362     * @param object|array $contact
363     * @return ThinnedContact
364     */
365    public static function contactToThinnedContact($contact): ThinnedContact
366    {
367        if (is_array($contact)) {
368            return new ThinnedContact(city: $contact['city'] ?? null, country: $contact['country'] ?? null, name: $contact['name'] ?? null, postalCode: $contact['postalCode'] ?? null, region: $contact['region'] ?? null, street: $contact['street'] ?? null, streetNumber: $contact['streetNumber'] ?? null);
369        }
370
371        return new ThinnedContact(city: $contact->city ?? null, country: $contact->country ?? null, name: $contact->name ?? null, postalCode: $contact->postalCode ?? null, region: $contact->region ?? null, street: $contact->street ?? null, streetNumber: $contact->streetNumber ?? null);
372    }
373
374    /**
375     * Convert a Provider object to a ThinnedProvider.
376     *
377     * @param Provider $provider
378     * @return ThinnedProvider
379     */
380    public static function providerToThinnedProvider(Provider $provider): ThinnedProvider
381    {
382        return new ThinnedProvider(id: isset($provider->id) ? (int) $provider->id : null, name: isset($provider->name) ? $provider->name : null, source: isset($provider->source) ? $provider->source : null, lon: isset($provider->data['geo']['lon']) ? (float) $provider->data['geo']['lon'] : null, lat: isset($provider->data['geo']['lat']) ? (float) $provider->data['geo']['lat'] : null, contact: isset($provider->contact) ? self::contactToThinnedContact($provider->contact) : null);
383    }
384}