Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.88% covered (success)
95.88%
256 / 267
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProcessSearch
95.88% covered (success)
95.88%
256 / 267
75.00% covered (warning)
75.00%
9 / 12
58
0.00% covered (danger)
0.00%
0 / 1
 mapSearchRowToProcess
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
 readSearch
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 readSearchCount
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 buildSearchQuery
90.57% covered (success)
90.57%
48 / 53
0.00% covered (danger)
0.00%
0 / 1
10.08
 extractSearchQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 addSearchConditions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addBasicSearchConditions
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
10
 addAdditionalSearchConditions
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
11
 resolveSearchReferences
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 buildCombinedSearchSql
98.48% covered (success)
98.48%
65 / 66
0.00% covered (danger)
0.00%
0 / 1
8
 buildCombinedSearchCountSql
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getHistoryAppointmentFrom
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
4.16
1<?php
2
3namespace BO\Zmsbackend\ProcessSearch\Service;
4
5use BO\Zmsbackend\ProcessSearch\Repository\ProcessSearch as ProcessSearchRepository;
6use BO\Zmsbackend\Query\Base as QueryBase;
7use BO\Zmsentities\Collection\ProcessList as Collection;
8use BO\Zmsentities\Process as Entity;
9use BO\Zmsbackend\Config\Service\Config as ConfigService;
10
11class ProcessSearch extends \BO\Zmsbackend\Base
12{
13    private const int DEFAULT_HISTORY_DAYS = 90;
14
15    public function mapSearchRowToProcess(array $row): Entity
16    {
17        return new Entity([
18            'id' => (int) $row['process_id'],
19            'displayNumber' => (string) ($row['display_number'] ?? ''),
20
21            'clients__0__familyName' => (string) ($row['citizen_name'] ?? ''),
22            'clients__0__telephone' => (string) ($row['telephone'] ?? ''),
23            'clients__0__email' => (string) ($row['citizen_email'] ?? ''),
24
25            'amendment' => (string) ($row['amendment'] ?? ''),
26
27            'appointments__0__date' => !empty($row['appointment_at'])
28                ? strtotime($row['appointment_at'])
29                : 0,
30
31            'appointments__0__scope__id' => (int) $row['scope_id'],
32
33            'createTimestamp' => ($row['source'] ?? '') === 'active'
34                ? (int) ($row['booked_at'] ?? 0)
35                : (!empty($row['booked_at'])
36                    ? strtotime($row['booked_at'])
37                    : 0),
38
39            'queue__callTime' => !empty($row['called_at'])
40                ? strtotime($row['called_at'])
41                : 0,
42
43            'scope__id' => (int) $row['scope_id'],
44            'scope__shortName' => (string) ($row['location_name'] ?? ''),
45            'scope__contact__name' => (string) ($row['provider_name'] ?? ''),
46
47            'status' => (string) $row['technical_status'],
48            'source' => (string) $row['source'],
49            'appointmentStatus' => (string) $row['appointment_status'],
50        ]);
51    }
52
53    public function readSearch(
54        array $parameter,
55        int $resolveReferences = 0,
56        int $limit = 100,
57        int $offset = 0
58    ): Collection {
59        $combined = $this->buildCombinedSearchSql(
60            $parameter,
61            $this->getHistoryAppointmentFrom(),
62            (int) $limit,
63            (int) $offset
64        );
65
66        $rows = $this->fetchAll(
67            $combined['sql'],
68            $combined['parameters']
69        );
70
71        $processList = new Collection();
72
73        foreach ($rows as $row) {
74            $entity = $this->mapSearchRowToProcess($row);
75
76            $entity = $this->resolveSearchReferences(
77                $entity,
78                $resolveReferences
79            );
80
81            $processList->addEntity($entity);
82        }
83
84        return $processList;
85    }
86
87    public function readSearchCount(array $parameter): int
88    {
89        $combined = $this->buildCombinedSearchCountSql(
90            $parameter,
91            $this->getHistoryAppointmentFrom()
92        );
93
94        return (int) $this->fetchValue(
95            $combined['sql'],
96            $combined['parameters']
97        );
98    }
99
100    protected function buildSearchQuery(
101        array $parameter
102    ): ProcessSearchRepository {
103        $query = new ProcessSearchRepository(QueryBase::SELECT);
104
105        $query
106            ->addConditionAssigned()
107            ->addConditionIgnoreSlots()
108            ->addConditionActiveSearchStatuses();
109
110        if (!empty($parameter['upcomingOnly'])) {
111            $now = class_exists('\App') && isset(\App::$now)
112                ? \App::$now
113                : new \DateTimeImmutable(
114                    'now',
115                    new \DateTimeZone('Europe/Berlin')
116                );
117
118            $query->addConditionUpcomingOnly($now);
119        }
120
121        if (array_key_exists('scopeIds', $parameter)) {
122            $scopeIds = is_array($parameter['scopeIds'])
123                ? $parameter['scopeIds']
124                : array_map(
125                    'intval',
126                    explode(',', (string) $parameter['scopeIds'])
127                );
128
129            $scopeIds = array_values(
130                array_filter($scopeIds)
131            );
132
133            $query->addConditionScopeIds($scopeIds);
134        }
135
136        if (isset($parameter['query'])) {
137            if (preg_match('#^\d+$#', $parameter['query'])) {
138                $query->addConditionProcessId(
139                    $parameter['query']
140                );
141
142                $query->addConditionSearch(
143                    $parameter['query'],
144                    true
145                );
146            } else {
147                $query->addConditionSearch(
148                    $parameter['query']
149                );
150            }
151
152            unset($parameter['query']);
153        }
154
155        if (count($parameter)) {
156            foreach (
157                [
158                    'upcomingOnly',
159                    'scopeIds',
160                    'page',
161                    'limit',
162                    'offset',
163                    'includePast',
164                    'denyHistory',
165                ] as $reservedKey
166            ) {
167                unset($parameter[$reservedKey]);
168            }
169
170            $query = $this->addSearchConditions(
171                $query,
172                $parameter
173            );
174        }
175
176        return $query;
177    }
178
179    protected function extractSearchQuery(array $parameter): ?string
180    {
181        if (!isset($parameter['query'])) {
182            return null;
183        }
184
185        $queryString = trim((string) $parameter['query']);
186
187        return $queryString !== '' ? $queryString : null;
188    }
189
190    protected function addSearchConditions(
191        ProcessSearchRepository $query,
192        array $parameter
193    ): ProcessSearchRepository {
194        $this->addBasicSearchConditions(
195            $query,
196            $parameter
197        );
198
199        $this->addAdditionalSearchConditions(
200            $query,
201            $parameter
202        );
203
204        return $query;
205    }
206
207    private function addBasicSearchConditions(
208        ProcessSearchRepository $query,
209        array $parameter
210    ): void {
211        if (isset($parameter['processId']) && $parameter['processId']) {
212            $query->addConditionProcessId(
213                $parameter['processId']
214            );
215        }
216
217        if (isset($parameter['name']) && $parameter['name']) {
218            $exact = isset($parameter['exact'])
219                ? $parameter['exact']
220                : false;
221
222            $query->addConditionName(
223                $parameter['name'],
224                $exact
225            );
226        }
227
228        if (isset($parameter['amendment']) && $parameter['amendment']) {
229            $query->addConditionAmendment(
230                $parameter['amendment']
231            );
232        }
233
234        if (isset($parameter['scopeId']) && $parameter['scopeId']) {
235            $query->addConditionScopeId(
236                $parameter['scopeId']
237            );
238        }
239    }
240
241    private function addAdditionalSearchConditions(
242        ProcessSearchRepository $query,
243        array $parameter
244    ): void {
245        if (isset($parameter['authKey']) && $parameter['authKey']) {
246            $query->addConditionAuthKey(
247                $parameter['authKey']
248            );
249        }
250
251        if (isset($parameter['requestId']) && $parameter['requestId']) {
252            $query->addConditionRequestId(
253                $parameter['requestId']
254            );
255        }
256
257        if (isset($parameter['provider']) && $parameter['provider']) {
258            $query->addConditionScopeNameSearch(
259                $parameter['provider']
260            );
261        }
262
263        if (isset($parameter['service']) && $parameter['service']) {
264            $query->addConditionServiceNameSearch(
265                $parameter['service']
266            );
267        }
268
269        if (isset($parameter['date']) && $parameter['date']) {
270            $query->addConditionDate(
271                $parameter['date']
272            );
273        }
274    }
275
276    protected function resolveSearchReferences(
277        Entity $entity,
278        int $resolveReferences
279    ): Entity {
280        if (($entity->source ?? 'active') === 'history') {
281            return $entity;
282        }
283
284        (new \BO\Zmsbackend\Process\Service\Process())
285            ->readResolvedReferences(
286                $entity,
287                $resolveReferences
288            );
289
290        return $entity;
291    }
292
293    protected function buildCombinedSearchSql(
294        array $parameter,
295        ?\DateTimeInterface $appointmentFrom = null,
296        ?int $limit = null,
297        int $offset = 0
298    ): array {
299        $searchRepository = $this->buildSearchQuery(
300            $parameter
301        );
302
303        $searchRepository->addCombinedActiveProjection();
304
305        $scopeIds = null;
306
307        if (array_key_exists('scopeIds', $parameter)) {
308            $scopeIds = is_array($parameter['scopeIds'])
309                ? $parameter['scopeIds']
310                : explode(
311                    ',',
312                    (string) $parameter['scopeIds']
313                );
314
315            $scopeIds = array_values(
316                array_filter(
317                    array_map(
318                        'intval',
319                        $scopeIds
320                    )
321                )
322            );
323        }
324
325        $searchQuery = $this->extractSearchQuery(
326            $parameter
327        );
328
329        $denyHistory =
330            !empty($parameter['denyHistory'])
331            || !empty($parameter['authKey'])
332            || !empty($parameter['requestId'])
333            || !empty($parameter['upcomingOnly']);
334
335        $historyParameters = [];
336
337        $historySql = $searchRepository->getHistorySelectSql(
338            $scopeIds,
339            $appointmentFrom,
340            $searchQuery,
341            $historyParameters,
342            $parameter['date'] ?? null,
343            $parameter['provider'] ?? null,
344            $parameter['service'] ?? null,
345            [
346                'name' => $parameter['name'] ?? null,
347                'exact' => !empty($parameter['exact']),
348                'amendment' => $parameter['amendment'] ?? null,
349                'processId' => $parameter['processId'] ?? null,
350                'scopeId' => $parameter['scopeId'] ?? null,
351                'denyHistory' => $denyHistory,
352            ]
353        );
354
355        $combinedSql = $searchRepository->getCombinedSelectSql(
356            $historySql
357        );
358
359        $sql = '
360            SELECT *
361            FROM (
362                ' . $combinedSql . '
363            ) combined
364            ORDER BY
365                combined.appointment_at DESC,
366                combined.source_record_id DESC,
367                combined.source ASC
368        ';
369
370        if ($limit !== null) {
371            $limit = max(0, $limit);
372            $offset = max(0, $offset);
373
374            $sql .= ' LIMIT ' . $limit;
375
376            if ($offset > 0) {
377                $sql .= ' OFFSET ' . $offset;
378            }
379        }
380
381        $parameters = array_merge(
382            $searchRepository->getParameters(),
383            $historyParameters
384        );
385
386        return [
387            'sql' => $sql,
388            'parameters' => $parameters,
389        ];
390    }
391
392    protected function buildCombinedSearchCountSql(
393        array $parameter,
394        ?\DateTimeInterface $appointmentFrom = null
395    ): array {
396        $combined = $this->buildCombinedSearchSql(
397            $parameter,
398            $appointmentFrom
399        );
400
401        $sql = '
402            SELECT COUNT(*) AS total_count
403            FROM (
404                ' . $combined['sql'] . '
405            ) combined_count
406        ';
407
408        return [
409            'sql' => $sql,
410            'parameters' => $combined['parameters'],
411        ];
412    }
413
414    protected function getHistoryAppointmentFrom(): \DateTimeImmutable
415    {
416        $now = class_exists('\App') && isset(\App::$now)
417            ? \App::$now
418            : new \DateTimeImmutable(
419                'now',
420                new \DateTimeZone('Europe/Berlin')
421            );
422
423        $config = (new ConfigService())->readEntity();
424
425        $retentionDays = filter_var(
426            $config->getPreference(
427                'processSearchHistory',
428                'deleteOlderThanDays'
429            ),
430            FILTER_VALIDATE_INT,
431            [
432                'options' => [
433                    'min_range' => 1,
434                ],
435            ]
436        );
437
438        if ($retentionDays === false) {
439            $retentionDays = self::DEFAULT_HISTORY_DAYS;
440        }
441
442        return \DateTimeImmutable::createFromInterface($now)
443            ->modify('-' . $retentionDays . ' days');
444    }
445}