Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.61% covered (warning)
81.61%
142 / 174
50.00% covered (danger)
50.00%
7 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProcessSearchHistory
81.61% covered (warning)
81.61%
142 / 174
50.00% covered (danger)
50.00%
7 / 14
38.37
0.00% covered (danger)
0.00%
0 / 1
 writeHistoryEntry
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
4.06
 existsByHistoryKey
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 readFullyResolvedProcess
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
4.37
 buildHistoryEntryData
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
1
 createHistoryKey
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
4.18
 buildServiceNames
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 getScope
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dateTimeFromTimestamp
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getNullableString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 validateStatus
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 isDuplicateKeyException
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getAppointmentAt
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
2.86
 getScopeId
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
3.19
 deleteOlderThan
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace BO\Zmsbackend\ProcessSearchHistory\Service;
4
5use BO\Zmsbackend\Exception\Pdo\PDOFailed;
6use BO\Zmsbackend\Process\Service\Process as ProcessService;
7use BO\Zmsbackend\ProcessSearchHistory\Repository\ProcessSearchHistory as HistoryRepository;
8use BO\Zmsbackend\Query\Base as QueryBase;
9use BO\Zmsentities\Process as ProcessEntity;
10use BO\Zmsentities\Scope as ScopeEntity;
11
12class ProcessSearchHistory extends \BO\Zmsbackend\Base
13{
14    public const string STATUS_COMPLETED = 'completed';
15    public const string STATUS_MISSED = 'missed';
16
17    private const int RESOLVE_REFERENCES = 2;
18    private const int DUPLICATE_KEY_ERROR_CODE = 1062;
19
20    /**
21     * Bei einem bereits vorhandenen history_key wird kein zweiter
22     * Datensatz angelegt. In beiden Fällen wird der history_key
23     * zurückgegeben.
24     */
25    public function writeHistoryEntry(
26        ProcessEntity $process,
27        string $status,
28        \DateTimeInterface $finalizedAt
29    ): string {
30        $this->validateStatus($status);
31
32        $process = $this->readFullyResolvedProcess($process);
33        $historyKey = $this->createHistoryKey($process);
34
35        if ($this->existsByHistoryKey($historyKey)) {
36            return $historyKey;
37        }
38
39        $query = new HistoryRepository(QueryBase::INSERT);
40        $query->addValuesNewHistory(
41            $this->buildHistoryEntryData(
42                $process,
43                $historyKey,
44                $status,
45                $finalizedAt
46            )
47        );
48
49        try {
50            $this->writeItem($query);
51        } catch (PDOFailed $exception) {
52            if (!$this->isDuplicateKeyException($exception)) {
53                throw $exception;
54            }
55        }
56
57        return $historyKey;
58    }
59
60    public function existsByHistoryKey(string $historyKey): bool
61    {
62        $query = new HistoryRepository(QueryBase::SELECT);
63        $query
64            ->addCountValue()
65            ->addConditionHistoryKey($historyKey);
66
67        return (int) $this->fetchValue(
68            $query,
69            $query->getParameters()
70        ) > 0;
71    }
72
73    private function readFullyResolvedProcess(
74        ProcessEntity $process
75    ): ProcessEntity {
76        if (!$process->hasProcessCredentials()) {
77            throw new \InvalidArgumentException(
78                'Process id and authKey are required for the history snapshot.'
79            );
80        }
81
82        $processService = new ProcessService(
83            $this->getWriter(),
84            $this->getReader()
85        );
86
87        $resolvedProcess = $processService->readEntity(
88            $process->getId(),
89            $process->getAuthKey(),
90            self::RESOLVE_REFERENCES
91        );
92
93        if ($resolvedProcess === null || !$resolvedProcess->hasId()) {
94            throw new \RuntimeException(
95                sprintf(
96                    'Process %d could not be loaded for the history snapshot.',
97                    $process->getId()
98                )
99            );
100        }
101
102        return $resolvedProcess;
103    }
104
105    private function buildHistoryEntryData(
106        ProcessEntity $process,
107        string $historyKey,
108        string $status,
109        \DateTimeInterface $finalizedAt
110    ): array {
111        $client = $process->getFirstClient();
112        $scope = $this->getScope($process);
113
114        return [
115            'historyKey' => $historyKey,
116
117            'processId' => (int) $process->getId(),
118            'scopeId' => $this->getScopeId($process),
119            'displayNumber' => $this->getNullableString(
120                $process->getDisplayNumber()
121            ),
122
123            'appointmentAt' => $this->getAppointmentAt($process),
124            'bookedAt' => $this->dateTimeFromTimestamp(
125                (int) $process['createTimestamp']
126            ),
127            'calledAt' => $this->dateTimeFromTimestamp(
128                (int) $process
129                    ->toProperty()
130                    ->queue
131                    ->callTime
132                    ->get(0)
133            ),
134
135            'finalizedAt' => $finalizedAt,
136            'status' => $status,
137
138            'citizenName' => (string) $client
139                ->toProperty()
140                ->familyName
141                ->get(''),
142
143            'telephone' => (string) $client
144                ->toProperty()
145                ->telephone
146                ->get(''),
147
148            'citizenEmail' => (string) $client
149                ->toProperty()
150                ->email
151                ->get(''),
152
153            'amendment' => $this->getNullableString(
154                $process->getAmendment()
155            ),
156
157            'locationName' => trim(
158                (string) $scope->getShortName()
159            ),
160
161            'providerName' => trim(
162                (string) $scope->getProvider()->getName()
163            ),
164
165            'services' => $this->buildServiceNames($process),
166        ];
167    }
168
169    private function createHistoryKey(
170        ProcessEntity $process
171    ): string {
172        $processId = (int) $process->getId();
173        $createTimestamp = (int) $process['createTimestamp'];
174        $authKey = (string) $process->getAuthKey();
175
176        if (
177            $processId <= 0
178            || $createTimestamp <= 0
179            || $authKey === ''
180        ) {
181            throw new \InvalidArgumentException(
182                'Process id, createTimestamp and authKey are required '
183                . 'to create the history key.'
184            );
185        }
186
187        return hash(
188            'sha256',
189            $processId
190                . '|'
191                . $createTimestamp
192                . '|'
193                . $authKey
194        );
195    }
196
197    private function buildServiceNames(
198        ProcessEntity $process
199    ): ?string {
200        $serviceNames = [];
201
202        foreach ($process->getRequests() as $request) {
203            $serviceName = trim((string) $request->getName());
204
205            if ($serviceName === '') {
206                continue;
207            }
208
209            $serviceNames[$serviceName] = true;
210        }
211
212        if ($serviceNames === []) {
213            return null;
214        }
215
216        return implode(
217            "\n",
218            array_keys($serviceNames)
219        );
220    }
221
222    private function getScope(
223        ProcessEntity $process
224    ): ScopeEntity {
225        return $process->getCurrentScope();
226    }
227
228    private function dateTimeFromTimestamp(
229        int $timestamp
230    ): ?\DateTimeImmutable {
231        if ($timestamp <= 0) {
232            return null;
233        }
234
235        return (new \DateTimeImmutable())
236            ->setTimestamp($timestamp);
237    }
238
239    private function getNullableString(
240        mixed $value
241    ): ?string {
242        $value = trim((string) $value);
243
244        return $value !== '' ? $value : null;
245    }
246
247    private function validateStatus(string $status): void
248    {
249        if (
250            !in_array(
251                $status,
252                [
253                    self::STATUS_COMPLETED,
254                    self::STATUS_MISSED,
255                ],
256                true
257            )
258        ) {
259            throw new \InvalidArgumentException(
260                sprintf(
261                    'Unsupported process search history status "%s".',
262                    $status
263                )
264            );
265        }
266    }
267
268    private function isDuplicateKeyException(
269        PDOFailed $exception
270    ): bool {
271        $previous = $exception->getPrevious();
272
273        if (!$previous instanceof \PDOException) {
274            return false;
275        }
276
277        return (int) ($previous->errorInfo[1] ?? 0)
278            === self::DUPLICATE_KEY_ERROR_CODE;
279    }
280
281    private function getAppointmentAt(
282        ProcessEntity $process
283    ): \DateTimeImmutable {
284        $appointment = $process->getFirstAppointment();
285        $timestamp = $appointment->toDateTime()->getTimestamp();
286
287        if ($timestamp <= 0) {
288            throw new \InvalidArgumentException(
289                sprintf(
290                    'Process %d has no valid appointment date.',
291                    $process->getId()
292                )
293            );
294        }
295
296        return $appointment->toDateTime();
297    }
298
299    private function getScopeId(ProcessEntity $process): int
300    {
301        $scopeId = (int) $process->getScopeId();
302
303        if ($scopeId <= 0) {
304            throw new \InvalidArgumentException(
305                sprintf(
306                    'Process %d has no valid scope.',
307                    $process->getId()
308                )
309            );
310        }
311
312        return $scopeId;
313    }
314
315    public function deleteOlderThan(
316        \DateTimeInterface $dateTime
317    ): bool {
318        $query = new
319            \BO\Zmsbackend\ProcessSearchHistory\Repository\ProcessSearchHistory(
320                \BO\Zmsbackend\Query\Base::DELETE
321            );
322
323        $query->addConditionOlderThan($dateTime);
324
325        return $this->deleteItem($query);
326    }
327}