Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.56% covered (warning)
82.56%
426 / 516
72.34% covered (warning)
72.34%
34 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
Process
82.56% covered (warning)
82.56%
426 / 516
72.34% covered (warning)
72.34%
34 / 47
256.56
0.00% covered (danger)
0.00%
0 / 1
 readEntity
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 readById
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 readResolvedReferences
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 updateEntity
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 updateEntityDisplayNumber
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 updateEntityWithSlots
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
6.14
 redirectToScope
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 readSlotCount
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 writeEntityWithNewAppointment
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 updateFollowingProcesses
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 updateReassignedRequests
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 writeNewProcess
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 readNewProcessId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 readAuthKeyByProcessId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 readList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 readProcessesWithoutRequests
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 attachAllRequestsInOneQuery
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 readEntityList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 readByWorkstation
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 readProcessListByScopesAndTime
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 readConflictListByScopeAndTime
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 readProcessListByScopeAndStatus
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 readProcessListByClusterAndTime
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 readProcessListCountByScope
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 readProcessListByMailAddress
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 readListByMailAndStatusList
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 updateProcessStatus
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 shouldUpdateDisplayNumber
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
6.32
 writeDeletedEntity
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 deleteEntity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 writeCanceledEntity
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 writeBlockedEntity
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 writeRequestsToDb
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 deleteRequestsForProcessId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 readExpiredProcessList
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 readUnconfirmedProcessList
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 readExpiredProcessListByStatus
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 readExpiredReservationsList
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 readEmailReminderProcessListByInterval
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 readDeallocateProcessList
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 isAppointmentAllowedWithSameMail
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
10.02
 isAppointmentSlotCountAllowed
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 isServiceQuantityAllowed
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
156
 isMailWhitelisted
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 readProcessWithSameDayAndDisplayNumber
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 readProcessListByExternalUserId
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 readAssignedWorkstationIdForUpdate
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php
2
3namespace BO\Zmsbackend\Process\Service;
4
5use BO\Zmsentities\Collection\AppointmentList;
6use BO\Zmsentities\Process as Entity;
7use BO\Zmsentities\Scope as ScopeEntity;
8use BO\Zmsentities\Collection\ProcessList as Collection;
9use BO\Zmsbackend\Helper\ProcessStatus;
10
11/**
12 *
13 * @SuppressWarnings(CouplingBetweenObjects)
14 * @SuppressWarnings(TooManyPublicMethods)
15 * @SuppressWarnings(Complexity)
16 * @SuppressWarnings(TooManyMethods)
17 */
18class Process extends \BO\Zmsbackend\Base implements \BO\Zmsbackend\Interfaces\ResolveReferences
19{
20    /**
21     * @param \BO\Zmsbackend\Helper\NoAuth|null|string $authKey
22     * @param int|null|string $processId
23     */
24    public function readEntity(int|string|null $processId = null, string|\BO\Zmsbackend\Helper\NoAuth|null $authKey = null, int $resolveReferences = 2): ?Entity
25    {
26        if (null === $processId || null === $authKey) {
27            return null;
28        }
29        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
30        $query->addEntityMapping()
31            ->addResolvedReferences($resolveReferences)
32            ->addConditionProcessId($processId);
33        if (!$authKey instanceof \BO\Zmsbackend\Helper\NoAuth) {
34            $query->addConditionAuthKey($authKey);
35        }
36        $process = $this->fetchOne($query, new Entity());
37        $process = $this->readResolvedReferences($process, $resolveReferences);
38
39        return $process;
40    }
41
42    public function readById($processId, $resolveReferences = 1): Entity
43    {
44        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
45        $query->addEntityMapping()
46            ->addResolvedReferences($resolveReferences)
47            ->addConditionProcessId($processId);
48        return $this->fetchOne($query, new Entity());
49    }
50
51    /**
52     *
53     * @return \BO\Zmsentities\Schema\Entity
54     */
55    #[\Override]
56    public function readResolvedReferences(\BO\Zmsentities\Schema\Entity $entity, int $resolveReferences)
57    {
58        if (0 <= $resolveReferences) {
59            if ($entity->archiveId) {
60                $entity->requests = (new \BO\Zmsbackend\Request\Service\Request())
61                    ->readRequestByArchiveId($entity->archiveId, $resolveReferences - 1);
62            } else {
63                $entity->requests = (new \BO\Zmsbackend\Request\Service\Request())
64                    ->readRequestByProcessId($entity->id, $resolveReferences - 1);
65            }
66        }
67        return $entity;
68    }
69
70
71    /**
72     * Update a process without changing appointment or scope
73     *
74     * @param null|string $previousStatus
75     *
76     */
77    public function updateEntity(\BO\Zmsentities\Process $process, \DateTimeInterface $now, int $resolveReferences = 0, string|null $previousStatus = null, ?\BO\Zmsentities\Useraccount $useraccount = null): Entity
78    {
79        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::UPDATE);
80        $query->addConditionProcessId($process->getId());
81        $query->addConditionAuthKey($process->getAuthKey());
82        $query->addValuesUpdateProcess($process, $now, 0, $previousStatus);
83        if ($this->perform($query->getLockProcessId(), ['processId' => $process->getId()])) {
84            $this->writeItem($query);
85            $this->writeRequestsToDb($process);
86        }
87        $process = $this->readEntity($process->getId(), $process->authKey, $resolveReferences);
88        if (!$process->getId()) {
89            throw new \BO\Zmsbackend\Process\Exception\ProcessUpdateFailed();
90        }
91        $this->perform(\BO\Zmsbackend\Process\Repository\Process::QUERY_UPDATE_FOLLOWING_PROCESS, [
92            'reserved' => ($process->status == 'reserved') ? 1 : 0,
93            'processID' => $process->getId(),
94        ]);
95        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("UPDATE (\BO\Zmsbackend\Process\Service\Process::updateEntity) $process ", \BO\Zmsbackend\Log\Service\Log::ACTION_EDITED, $process, $useraccount);
96        return $process;
97    }
98
99    public function updateEntityDisplayNumber(Entity $process): Entity
100    {
101        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::UPDATE);
102        $query->addConditionProcessId($process->getId());
103        $query->addConditionAuthKey($process->getAuthKey());
104
105        $process->displayNumber = $query->getNewDisplayNumber($process);
106        $query->addValueDisplayNumber($process);
107        if ($this->perform($query->getLockProcessId(), ['processId' => $process->getId()])) {
108            $this->writeItem($query);
109        }
110
111        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("UPDATE (updateEntityDisplayNumber) $process ", \BO\Zmsbackend\Log\Service\Log::ACTION_EDITED, $process);
112        return $process;
113    }
114
115    /**
116     * Update a process with overbooked slots
117     *
118     * @param \BO\Zmsentities\Process $process
119     * @param \DateTimeInterface $now
120     * @param Int $slotsRequired we cannot use process.appointments.0.slotCount, because setting slotsRequired is
121     *        a priviliged operation. Just using the input would be a security flaw to get a wider selection of times
122     *        If slotsRequired = 0, readFreeProcesses() uses the slotsRequired based on request-provider relation
123     * @psalm-api
124     */
125    public function updateEntityWithSlots(\BO\Zmsentities\Process $process, \DateTimeInterface $now, $slotType = "intern", $slotsRequired = 0, $resolveReferences = 0, $userAccount = null)
126    {
127        if ('intern' != $slotType) {
128            return $this->updateEntity($process, $now, $resolveReferences, null, $userAccount);
129        }
130        $process = clone $process;
131        $appointment = $process->getAppointments()->getFirst();
132        $slotList = (new \BO\Zmsbackend\Slot\Service\Slot())->readByAppointment($appointment, $slotsRequired, true);
133        $processEntityList = $this->readEntityList($process->getId());
134        foreach ($processEntityList as $entity) {
135            if ($process->getId() != $entity->getId()) {
136                $this->writeDeletedEntity($entity->getId());
137            }
138        }
139
140        foreach ($slotList as $slot) {
141            $newProcess = clone $process;
142            $newProcess->getFirstAppointment()->setTime($slot->time);
143            if (! $newProcess->getFirstAppointment()->isMatching($process->getFirstAppointment())) {
144                $this->writeNewProcess($newProcess, $now, $process->id, 0, true, $userAccount);
145            }
146        }
147
148        $appointment->addSlotCount($slotList->count());
149
150        $processEntity = $this->updateEntity($process, $now, $resolveReferences, null, $userAccount);
151        $process = $this->readEntity($process->getId(), $process->authKey, $resolveReferences);
152
153        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("CREATE (\BO\Zmsbackend\Process\Service\Process::updateEntityWithSlots) $process ", \BO\Zmsbackend\Log\Service\Log::ACTION_EDITED, $process, $userAccount);
154        return $processEntity;
155    }
156
157    public function redirectToScope(Entity $process, \BO\Zmsentities\Scope $scope, int $waitingNumber, ?\BO\Zmsentities\Useraccount $useraccount = null): Entity
158    {
159        $datetime = \App::$now;
160        $process->setStatus('confirmed');
161        $appointment = $process->getFirstAppointment();
162        $date = (new \DateTimeImmutable())->setTimestamp($appointment->date);
163        $date = $date->setTime(0, 0, 0);
164        $appointment->date = $date->getTimestamp();
165        $process->appointments = new AppointmentList([$appointment]);
166        $newQueueNumber = (new \BO\Zmsbackend\Scope\Service\Scope())->readWaitingNumberUpdated($scope->id, $datetime);
167        $process->addQueue($newQueueNumber, $datetime);
168        $process->queue['number'] = $waitingNumber;
169        $process->priority = \BO\Zmsentities\Collection\QueueList::DEFAULT_PRIORITY_WITH_APPOINTMENT;
170        $process = $this->writeNewProcess($process, $datetime);
171        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("CREATE (\BO\Zmsbackend\Process\Service\Process::redirectToScope) $process ", \BO\Zmsbackend\Log\Service\Log::ACTION_REDIRECTED, $process, $useraccount);
172        $this->writeRequestsToDb($process);
173        return $process;
174    }
175
176    public function readSlotCount(\BO\Zmsentities\Process $process): Entity
177    {
178        $scope = new \BO\Zmsentities\Scope($process->scope);
179        $requestRelationList = (new \BO\Zmsbackend\RequestRelation\Service\RequestRelation())
180            ->readListByProviderId($scope->getProviderId(), $scope->getSource());
181        $appointment = $process->getAppointments()->getFirst();
182        $appointment->slotCount = 0;
183        foreach ($process->requests as $request) {
184            foreach ($requestRelationList as $requestRelation) {
185                if ($requestRelation->getRequest()->getId() == $request->getId()) {
186                    $appointment->slotCount += $requestRelation->getSlotCount();
187                }
188            }
189        }
190        $appointment->slotCount = round($appointment->slotCount, 0);
191        return $process;
192    }
193
194    /**
195     * write a new process with appointment and keep id and authkey from original process
196     */
197    public function writeEntityWithNewAppointment(\BO\Zmsentities\Process $process, \BO\Zmsentities\Appointment $appointment, \DateTimeInterface $now, $slotType = 'public', $slotsRequired = 0, $resolveReferences = 0, $keepReserved = false): Entity|null
198    {
199        // clone to new process with id = 0 and new appointment to reserve
200        $processNew = clone $process;
201        $processNew->id = 0;
202        $processNew->scope = $appointment->scope;
203        $processNew->queue['arrivalTime'] = 0;
204        $processNew->queue['number'] = 0;
205        $processNew->appointments = (new \BO\Zmsentities\Collection\AppointmentList())->addEntity($appointment);
206//delete old process with following processes
207        $this->writeDeletedEntity($process->getId());
208//reserve new appointment
209        $processNew = \BO\Zmsbackend\Process\Service\ProcessStatusFree::init()
210            ->writeEntityReserved($processNew, $now, $slotType, $slotsRequired);
211        $processTempNewId = $processNew->getId();
212// reassign credentials of new process with credentials of old process
213        $processNew->withReassignedCredentials($process);
214// update new process with old credentials, also assigned requests and following slots
215        $this->updateFollowingProcesses($processTempNewId, $processNew);
216        $this->updateReassignedRequests($processTempNewId, $processNew->getId());
217//delete slot mapping for temp process id
218        (new \BO\Zmsbackend\Slot\Service\Slot())->deleteSlotProcessMappingFor($processTempNewId);
219//write new slot mapping for changed process with old credentials because new appointment data
220        (new \BO\Zmsbackend\Slot\Service\Slot())->writeSlotProcessMappingFor($processNew->getId());
221        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("UPDATE (\BO\Zmsbackend\Process\Service\Process::writeEntityWithNewAppointment) $process ", \BO\Zmsbackend\Log\Service\Log::ACTION_EDITED, $process);
222        $status = ($keepReserved) ? Entity::STATUS_RESERVED : ENTITY::STATUS_CONFIRMED;
223        return $this->updateProcessStatus($processNew, $status, $now, $resolveReferences);
224    }
225
226    /**
227     * update following process with new credentials (also change process id if necessary)
228     */
229    public function updateFollowingProcesses($processId, \BO\Zmsentities\Process $processData): void
230    {
231        $this->perform(\BO\Zmsbackend\Process\Repository\Process::QUERY_REASSIGN_PROCESS_CREDENTIALS, [
232            'newProcessId' => $processData->getId(),
233            'newAuthKey' => $processData->getAuthKey(),
234            'processId' => $processId
235        ]);
236        $processEntityList = $this->readEntityList($processId);
237        if ($processEntityList->count()) {
238            foreach ($processEntityList as $entity) {
239                if ($entity->getId() != $processId) {
240                    $this->perform(\BO\Zmsbackend\Process\Repository\Process::QUERY_REASSIGN_FOLLWING_PROCESS, [
241                        'newProcessId' => $processData->getId(),
242                        'processId' => $processId
243                    ]);
244                }
245            }
246        }
247    }
248
249    /**
250     * update process requests with new credentials
251     */
252    public function updateReassignedRequests($processId, $newProcessId): void
253    {
254        $this->perform(\BO\Zmsbackend\Process\Repository\Process::QUERY_REASSIGN_PROCESS_REQUESTS, [
255            'newProcessId' => $newProcessId,
256            'processId' => $processId
257        ]);
258    }
259
260    /**
261     * write a new process to DB
262     *
263     */
264    protected function writeNewProcess(\BO\Zmsentities\Process $process, \DateTimeInterface $dateTime, int $parentProcess = 0, int $childProcessCount = 0, bool $retry = true, \BO\Zmsentities\Useraccount|null $userAccount = null)
265    {
266        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::INSERT);
267        $process->id = $this->readNewProcessId();
268        $process->setRandomAuthKey();
269        $process->createTimestamp = $dateTime->getTimestamp();
270        if ($parentProcess !== 0) {
271            $process->displayNumber = '';
272        }
273
274        $query->addValuesNewProcess($process, $parentProcess, $childProcessCount);
275
276        if ($parentProcess === 0 && !empty($process->getDisplayNumber())) {
277            $query->addValueDisplayNumber($process);
278        }
279
280        $query->addValuesScopeData($process);
281        $query->addValuesAppointmentData($process);
282        $query->addValuesUpdateProcess($process, $dateTime, $parentProcess);
283        try {
284            $this->writeItem($query);
285        } catch (\BO\Zmsbackend\Exception\Pdo\PDOFailed $exception) {
286            if ($retry) {
287        // First try might fail if two processes are created with the same number at the same time
288                sleep(1);
289        // Let the other process complete his transaction
290                return $this->writeNewProcess($process, $dateTime, $parentProcess, $childProcessCount, false, $userAccount);
291            }
292            throw new \BO\Zmsbackend\Process\Exception\ProcessCreateFailed($exception->getMessage());
293        }
294        (new \BO\Zmsbackend\Slot\Service\Slot())->writeSlotProcessMappingFor($process->id);
295        $checksum = ($userAccount) ? sha1($process->id . '-' . $userAccount->getId()) : '';
296
297        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("CREATE (\BO\Zmsbackend\Process\Service\Process::writeNewProcess) $process $checksum ", \BO\Zmsbackend\Log\Service\Log::ACTION_NEW, $process, $userAccount);
298        return $process;
299    }
300
301    /**
302     * Fetch a free process ID from DB
303     *
304     */
305    protected function readNewProcessId()
306    {
307        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
308        $newProcessId = $this->fetchValue($query->getQueryNewProcessId());
309        return $newProcessId;
310    }
311
312    /**
313     * Read auth data by processId (for request-side auth checks).
314     *
315     * @return array{authKey: string}|null
316     */
317    public function readAuthKeyByProcessId($processId)
318    {
319        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
320        $query
321            ->addEntityMapping()
322            ->addResolvedReferences(0)
323            ->addConditionProcessId($processId);
324        $process = $this->fetchOne($query, new Entity());
325        return ($process->hasId()) ? array(
326            'authKey' => $process->authKey
327        ) : null;
328    }
329
330    protected function readList($statement, $resolveReferences): Collection
331    {
332        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
333        $processList = new Collection();
334        while ($processData = $statement->fetch(\PDO::FETCH_ASSOC)) {
335            $entity = new Entity($query->postProcessJoins($processData));
336            $entity = $this->readResolvedReferences($entity, $resolveReferences);
337            $processList->addEntity($entity);
338        }
339        return $processList;
340    }
341
342    protected function readProcessesWithoutRequests($statement): Collection
343    {
344        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
345        $processList = new Collection();
346        while ($processData = $statement->fetch(\PDO::FETCH_ASSOC)) {
347            $processList->addEntity(new Entity($query->postProcessJoins($processData)));
348        }
349        return $processList;
350    }
351
352    protected function attachAllRequestsInOneQuery(Collection $processList, int $resolveReferences): Collection
353    {
354        if ($processList->count() === 0 || $resolveReferences < 0) {
355            return $processList;
356        }
357
358        $processIds = [];
359        foreach ($processList as $process) {
360            if ($process->hasId()) {
361                $processIds[] = $process->getId();
362            }
363        }
364
365        $requestsByProcessId = (new \BO\Zmsbackend\Request\Service\Request())
366            ->readAllRequestsForProcessIds($processIds, $resolveReferences - 1);
367
368        foreach ($processList as $process) {
369            if (!$process->hasId()) {
370                continue;
371            }
372            $process->requests = $requestsByProcessId[(int) $process->getId()]
373                ?? new \BO\Zmsentities\Collection\RequestList();
374        }
375
376        return $processList;
377    }
378
379    /**
380     * Read list with following processes in DB
381     */
382    public function readEntityList($processId, $resolveReferences = 0)
383    {
384        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
385        $query
386            ->addResolvedReferences($resolveReferences)
387            ->addEntityMapping()
388            ->addConditionProcessIdFollow($processId);
389        $statement = $this->fetchStatement($query);
390        return $this->readList($statement, $resolveReferences)->sortByAppointmentDate();
391    }
392
393    public function readByWorkstation(\BO\Zmsentities\Workstation $workstation, $resolveReferences = 0)
394    {
395        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
396        $query
397            ->addResolvedReferences($resolveReferences)
398            ->addEntityMapping()
399            ->addConditionWorkstationId($workstation->id);
400        $process = $this->fetchOne($query, new Entity());
401        return ($process->hasId()) ? $this->readResolvedReferences($process, $resolveReferences) : null;
402    }
403
404    /**
405     * Read processList by scopeId and DateTime
406     *
407     * @param array|int $scopeIds
408     * @param \DateTimeInterface $dateTime
409     *
410     * @return Collection
411     *
412     */
413    public function readProcessListByScopesAndTime(
414        $scopeIds,
415        \DateTimeInterface $dateTime,
416        int $resolveReferences = 0,
417        $withEntities = []
418    ) {
419        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT, '', false, null, $withEntities);
420        $query
421            ->addResolvedReferences($resolveReferences)
422            ->addEntityMapping()
423            ->addConditionScopeIds($scopeIds)
424            ->addConditionAssigned()
425            ->addConditionIgnoreSlots()
426            ->addConditionTime($dateTime)
427            ->removeDuplicates();
428        $statement = $this->fetchStatement($query);
429        return $this->readList($statement, $resolveReferences);
430    }
431
432    /**
433     * Read conflictList by scopeId and DateTime
434     *
435     * @param array|int $scopeIds
436     * @param \DateTimeInterface $dateTime
437     *
438     * @return Collection
439     */
440    public function readConflictListByScopeAndTime(\BO\Zmsentities\Scope $scope, \DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null, \DateTimeInterface $now = null, $resolveReferences = 1)
441    {
442        $availabilityList = (new \BO\Zmsbackend\Availability\Service\Availability())
443            ->readAvailabilityListByScope($scope, 0, $startDate, $endDate)
444            ->withScope($scope);
445        if (! $endDate) {
446            $availabilityList = $availabilityList->withDateTime($startDate);
447            $endDate = $startDate;
448        }
449        $currentDate = ($startDate) ? $startDate : $now;
450        $conflictList = $availabilityList->getConflicts($startDate, $endDate);
451        while ($currentDate <= $endDate) {
452            $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
453            $query
454                ->addResolvedReferences($resolveReferences)
455                ->addEntityMapping()
456                ->addConditionScopeId($scope->getId())
457                ->addConditionAssigned()
458                ->addConditionIgnoreSlots();
459            $query->addConditionTime($currentDate);
460            $statement = $this->fetchStatement($query);
461            $processList = $this->readList($statement, $resolveReferences);
462            $processList = $processList->toQueueList($currentDate)->withoutStatus(['queued'])->toProcessList();
463            $conflictList->addList($processList->withOutAvailability($availabilityList));
464            $currentDate = $currentDate->modify('+1 day');
465        }
466        $conflictList = $conflictList->withoutExpiredAppointmentDate($now);
467        return $conflictList;
468    }
469
470
471    /**
472     * Read processList by scopeId and status
473     *
474     * @return Collection
475     *
476     */
477    public function readProcessListByScopeAndStatus(int $scopeId, string $status, int $resolveReferences = 0, $limit = 1000, $offset = null)
478    {
479        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
480        $query
481            ->addResolvedReferences($resolveReferences)
482            ->addEntityMapping()
483            ->addConditionScopeId($scopeId) //removed because of dismatching scope and pickup scope
484            ->addConditionStatus($status)
485            ->addLimit($limit, $offset);
486        $statement = $this->fetchStatement($query);
487        return $this->readList($statement, $resolveReferences);
488    }
489
490    /**
491     * Read processList by clusterId and DateTime
492     *
493     * @param int $clusterId
494     * @param \DateTimeInterface $dateTime
495     *
496     * @return Collection
497     */
498    public function readProcessListByClusterAndTime($clusterId, \DateTimeInterface $dateTime)
499    {
500        $cluster = (new \BO\Zmsbackend\Cluster\Service\Cluster())->readEntity($clusterId, 1);
501        $scopeIds = [];
502        if ($cluster->scopes->count()) {
503            foreach ($cluster->scopes as $scope) {
504                $scopeIds[] = $scope->id;
505            }
506        }
507
508        return $this->readProcessListByScopesAndTime($scopeIds, $dateTime);
509    }
510
511    /**
512     * Read processList by scopeId to get a number of all processes of a scope
513     *
514     * @param int $scopeId
515     *
516     * @return Collection
517     */
518    public function readProcessListCountByScope($scopeId)
519    {
520        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
521        $query
522            ->addCountValue()
523            ->addConditionAssigned()
524            ->addConditionIgnoreSlots()
525            ->addConditionScopeId($scopeId);
526        $statement = $this->fetchStatement($query);
527        $result = $statement->fetch(\PDO::FETCH_ASSOC);
528        return $result['processCount'];
529    }
530
531    /**
532     * Read processList by mail address
533     *
534     * @return Collection
535     */
536    public function readProcessListByMailAddress(string $mailAddress, int $scopeId = null, $resolveReferences = 0, $limit = 2000): Collection
537    {
538        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
539        $query
540            ->addResolvedReferences($resolveReferences)
541            ->addEntityMapping()
542            ->addConditionMail($mailAddress, true)
543            ->addConditionIgnoreSlots()
544            ->addLimit($limit);
545        if ($scopeId) {
546            $query->addConditionScopeId($scopeId);
547        }
548
549        $statement = $this->fetchStatement($query);
550        return $this->readList($statement, $resolveReferences);
551    }
552
553    /**
554     * Read processList by mail address and statuslist
555     *
556     * @return Collection
557     *
558     */
559    public function readListByMailAndStatusList(string $mailAddress, array $statusList, int $resolveReferences = 1, int $limit = 300): Collection
560    {
561        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
562        $query
563            ->addResolvedReferences($resolveReferences)
564            ->addEntityMapping()
565            ->addConditionMail($mailAddress, true)
566            ->addConditionIgnoreSlots()
567            ->addLimit($limit);
568        $statement = $this->fetchStatement($query);
569        $collection = $this->readList($statement, $resolveReferences);
570        return $collection->toProcessListByStatusList($statusList);
571    }
572
573
574    /**
575     * Markiere einen Termin als bestätigt
576     *
577     * @param \BO\Zmsentities\Process $process
578     *
579     * @return Entity|null
580     *
581     */
582    public function updateProcessStatus(Entity $process, string $status, \DateTimeInterface $dateTime, $resolveReferences = 0, $userAccount = null)
583    {
584        $process = (new ProcessStatus())
585            ->writeUpdatedStatus($process, $status, $dateTime, $resolveReferences, $userAccount);
586
587        /** @var Entity $process */
588        if ($this->shouldUpdateDisplayNumber($process, $status)) {
589            $this->updateEntityDisplayNumber($process);
590        }
591
592        return $process;
593    }
594
595    public function shouldUpdateDisplayNumber(Entity $process, $status): bool
596    {
597        if ($status !== 'preconfirmed' && $status !== 'confirmed') {
598            return false;
599        }
600        $displayNumberPrefix = $process->scope->getPreference('queue', 'displayNumberPrefix');
601        if (empty($displayNumberPrefix)) {
602            return false;
603        }
604
605        if (str_starts_with($process->getDisplayNumber(), $displayNumberPrefix)) {
606            return false;
607        }
608
609        return true;
610    }
611
612    /**
613     * Löscht einen Termin aus der Datenbank
614     * Regulär sollte aber \BO\Zmsbackend\Process\Service\ProcessStatusArchived::writeEntityFinished()
615     * oder self::writeBlockedEntity() verwendet werden.
616     *
617     * @return bool
618     */
619    public function writeDeletedEntity($processId): bool
620    {
621        $processEntityList = $this->readEntityList($processId);
622        $status = false;
623        if ($processEntityList->count()) {
624            foreach ($processEntityList as $entity) {
625                $entityId = $entity->getId();
626                $query = \BO\Zmsbackend\Process\Repository\Process::QUERY_DELETE;
627                $status = $this->perform($query, array(
628                    $entityId,
629                    $entityId
630                ));
631                if ($status) {
632                            $this->deleteRequestsForProcessId($entityId);
633                            (new \BO\Zmsbackend\Slot\Service\Slot())->deleteSlotProcessMappingFor($entityId);
634                            \BO\Zmsbackend\Log\Service\Log::writeProcessLog("DELETE (\BO\Zmsbackend\Process\Service\Process::writeDeletedEntity) $entityId ", \BO\Zmsbackend\Log\Service\Log::ACTION_DELETED, $entity);
635                }
636            }
637        }
638
639        return $status;
640    }
641
642    /**
643     * ACHTUNG: Nur noch als Helferfunction vor Refactoring durch MF,
644     * damit unittests und zmsappointment wie gewohnt funktionieren
645     *
646     * @param int|string $processId
647     * @param string $authKey
648     *
649     * @return Entity|null
650     */
651    public function deleteEntity($processId, $authKey)
652    {
653        return $this->writeCanceledEntity($processId, $authKey);
654    }
655
656    /**
657     * Markiere einen Termin als abgesagt
658     *
659     * @param int|string $processId
660     * @param string $authKey
661     *
662     * @return Entity|null
663     */
664    public function writeCanceledEntity($processId, $authKey, $now = null, ?\BO\Zmsentities\Useraccount $useraccount = null)
665    {
666        $canceledTimestamp = ($now) ? $now->getTimestamp() : (new \DateTimeImmutable())->getTimestamp();
667        $newAuthKey = bin2hex(random_bytes(32));
668        $query = \BO\Zmsbackend\Process\Repository\Process::QUERY_CANCELED;
669        $this->perform($query, [
670            'processId' => $processId,
671            'authKey' => $authKey,
672            'canceledTimestamp' => $canceledTimestamp,
673            'newAuthKey' => $newAuthKey
674        ]);
675        $process = $this->readEntity($processId, new \BO\Zmsbackend\Helper\NoAuth(), 0);
676        \BO\Zmsbackend\Log\Service\Log::writeProcessLog("DELETE (\BO\Zmsbackend\Process\Service\Process::writeCanceledEntity) $processId ", \BO\Zmsbackend\Log\Service\Log::ACTION_CANCELED, $process, $useraccount);
677        return $process;
678    }
679
680    /**
681     * Markiere einen Termin als dereferenced
682     *
683     * @param \BO\Zmsentities\Process $process
684     * @param bool $releaseSlots
685     * @param \BO\Zmsentities\Useraccount|null $useraccount
686     *
687     * @return Entity|null
688     */
689    public function writeBlockedEntity(\BO\Zmsentities\Process $process, bool $releaseSlots = false, ?\BO\Zmsentities\Useraccount $useraccount = null)
690    {
691        $amendment = $process->toDerefencedAmendment();
692        $customTextfield = $process->toDerefencedcustomTextfield();
693        $customTextfield2 = $process->toDerefencedcustomTextfield2();
694        if (!isset($process->queue['status'])) {
695            $process->queue['status'] = $process->status;
696        }
697        $process->status = 'blocked';
698        $query = \BO\Zmsbackend\Process\Repository\Process::QUERY_DEREFERENCED;
699        $status = $this->perform($query, array(
700            $amendment,
701            $customTextfield,
702            $customTextfield2,
703            $process->id,
704            $process->authKey,
705            $process->id
706        ));
707        if ($status) {
708            $processEntityList = $this->readEntityList($process->id);
709            if ($processEntityList->count()) {
710                foreach ($processEntityList as $entity) {
711                    $entityId = $entity->getId();
712                    if ($releaseSlots) {
713                        (new \BO\Zmsbackend\Slot\Service\Slot())->deleteSlotProcessMappingFor($entityId);
714                    }
715                    \BO\Zmsbackend\Log\Service\Log::writeProcessLog("DELETE (\BO\Zmsbackend\Process\Service\Process::writeBlockedEntity) $entityId ", \BO\Zmsbackend\Log\Service\Log::ACTION_DELETED, $process, $useraccount);
716                }
717            }
718        }
719        return $status;
720    }
721
722    protected function writeRequestsToDb(\BO\Zmsentities\Process $process): void
723    {
724        // Beware of resolveReferences=0 to not delete the existing requests, except for queued processes
725        $hasRequests = ($process->requests && count($process->requests));
726        if ($hasRequests || 'queued' == $process->status) {
727            $this->deleteRequestsForProcessId($process->id);
728        }
729        if ($hasRequests) {
730            $query = new \BO\Zmsbackend\Request\Repository\XRequest(\BO\Zmsbackend\Query\Base::INSERT);
731            foreach ($process->requests as $request) {
732                if ($request->id >= 0) {
733        // allow deleting requests with a -1 request
734                    $query->addValues([
735                            'AnliegenID' => $request['id'],
736                            'source' => $request['source'],
737                            'BuergerID' => $process->id
738                        ]);
739                    $this->writeItem($query);
740                }
741            }
742        }
743    }
744
745    protected function deleteRequestsForProcessId($processId): bool
746    {
747        $status = false;
748        if (0 < $processId) {
749            $query = new \BO\Zmsbackend\Request\Repository\XRequest(\BO\Zmsbackend\Query\Base::DELETE);
750            $query->addConditionProcessId($processId);
751            $status = $this->deleteItem($query);
752        }
753        return $status;
754    }
755
756    public function readExpiredProcessList(\DateTimeInterface $expirationDate, $limit = 500, $resolveReferences = 0, $offset = null)
757    {
758        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
759        $selectQuery
760            ->addEntityMapping()
761            ->addResolvedReferences($resolveReferences)
762            ->addConditionProcessDeleteInterval($expirationDate)
763            ->addConditionIgnoreSlots()
764            ->addLimit($limit, $offset);
765        $statement = $this->fetchStatement($selectQuery);
766        return $this->readList($statement, $resolveReferences);
767    }
768    public function readUnconfirmedProcessList(\DateTimeInterface $expirationDate, $scopeId = 0, $limit = 500, $offset = null, $resolveReferences = 0)
769    {
770
771        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
772        $selectQuery
773            ->addEntityMapping()
774            ->addResolvedReferences($resolveReferences)
775            ->addConditionScopeId($scopeId)
776            ->addConditionProcessExpiredIPTimeStamp($expirationDate)
777            ->addConditionStatus('preconfirmed')
778            ->addConditionIgnoreSlots()
779            ->addLimit($limit, $offset);
780        $statement = $this->fetchStatement($selectQuery);
781        return $this->readList($statement, $resolveReferences);
782    }
783
784    public function readExpiredProcessListByStatus(\DateTimeInterface $expirationDate, $status, $limit = 500, $offset = null, $resolveReferences = 0)
785    {
786        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
787        $selectQuery
788            ->addEntityMapping()
789            ->addResolvedReferences($resolveReferences)
790            ->addConditionProcessDeleteBeforeDate($expirationDate)
791            ->addConditionStatus($status)
792            ->addConditionIgnoreSlots()
793            ->addLimit($limit, $offset);
794        $statement = $this->fetchStatement($selectQuery);
795        return $this->readList($statement, $resolveReferences);
796    }
797
798    public function readExpiredReservationsList(\DateTimeInterface $expirationDate, $scopeId, $limit = 500, $offset = null, $resolveReferences = 0)
799    {
800        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
801        $selectQuery
802            ->addEntityMapping()
803            ->addResolvedReferences($resolveReferences)
804            ->addConditionScopeId($scopeId)
805            ->addConditionIsReserved()
806            ->addConditionProcessExpiredIPTimeStamp($expirationDate)
807            ->addConditionIgnoreSlots()
808            ->addLimit($limit, $offset);
809        $statement = $this->fetchStatement($selectQuery);
810        return $this->readList($statement, $resolveReferences);
811    }
812
813    public function readEmailReminderProcessListByInterval(\DateTimeInterface $now, \DateTimeInterface $lastRun, $defaultReminderInMinutes, $limit = 500, $offset = null, int $resolveReferences = 0)
814    {
815        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
816        $selectQuery
817            ->addEntityMapping()
818            ->addResolvedReferences($resolveReferences)
819            ->addConditionProcessMailReminder($now, $lastRun, $defaultReminderInMinutes)
820            ->addConditionAssigned()
821            ->addConditionIgnoreSlots()
822            ->addConditionStatus('confirmed')
823            ->addLimit($limit, $offset);
824        $statement = $this->fetchStatement($selectQuery);
825        return $this->readList($statement, $resolveReferences)->withDepartmentHasMailFrom();
826    }
827
828    public function readDeallocateProcessList(\DateTimeInterface $now, $limit = 500, $offset = null, $resolveReferences = 0)
829    {
830        $selectQuery = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
831        $selectQuery
832            ->addEntityMapping()
833            ->addResolvedReferences($resolveReferences)
834            ->addConditionDeallocate($now)
835            ->addConditionIgnoreSlots()
836            ->addLimit($limit, $offset);
837        $statement = $this->fetchStatement($selectQuery);
838        return $this->readList($statement, $resolveReferences);
839    }
840
841    public function isAppointmentAllowedWithSameMail(Entity $entity): bool
842    {
843        if (empty($entity->getClients()) || empty($entity->getClients()->getFirst())) {
844            return true;
845        }
846
847        $maxAppointmentsPerMail = $entity->scope->getAppointmentsPerMail();
848        $emailToCheck = $entity->getClients()->getFirst()->email;
849        if ($maxAppointmentsPerMail < 1 || empty($emailToCheck)) {
850            return true;
851        }
852
853        if ($this->isMailWhitelisted($emailToCheck, $entity->scope)) {
854            return true;
855        }
856
857        $processes = $this->readProcessListByMailAddress($entity->getClients()->getFirst()->email, $entity->scope->id);
858        $activeAppointments = 0;
859        foreach ($processes as $process) {
860            if ($entity->id == $process->id) {
861                return true;
862            }
863
864            if (in_array($process->getStatus(), ['preconfirmed', 'confirmed'])) {
865                $activeAppointments++;
866                if ($activeAppointments >= $maxAppointmentsPerMail) {
867                    return false;
868                }
869            }
870        }
871
872        return true;
873    }
874
875    public function isAppointmentSlotCountAllowed(Entity $entity): bool
876    {
877        if (empty($entity->scope)) {
878            return true;
879        }
880
881        $maxSlotsPerAppointment = $entity->scope->getSlotsPerAppointment();
882
883        if ($maxSlotsPerAppointment === null || $maxSlotsPerAppointment < 1) {
884            $maxSlotsPerAppointment = \BO\Zmsbackend\Slot\Service\Slot::MAX_SLOTS;
885        }
886
887        $appointment = $entity->getAppointments()->getFirst();
888        if (!$appointment) {
889            return true;
890        }
891
892        $slotCount = (int) ($appointment->slotCount ?? 0);
893        if ($slotCount <= 0) {
894            return true;
895        }
896
897        return $slotCount <= (int) $maxSlotsPerAppointment;
898    }
899
900    public function isServiceQuantityAllowed(Entity $entity): bool
901    {
902        if (empty($entity->scope) || empty($entity->requests)) {
903            return true;
904        }
905
906        try {
907            $providerId = $entity->scope->getProviderId();
908        } catch (\Exception $e) {
909            return true;
910        }
911        if (!$providerId) {
912            return true;
913        }
914
915        $requestCounts = [];
916        foreach ($entity->requests as $request) {
917            $requestId = $request->getId();
918            if (!isset($requestCounts[$requestId])) {
919                $requestCounts[$requestId] = 0;
920            }
921            $requestCounts[$requestId]++;
922        }
923
924        $requestRelationDb = new \BO\Zmsbackend\RequestRelation\Service\RequestRelation();
925        foreach ($requestCounts as $requestId => $count) {
926            $requestRelation = $requestRelationDb->readEntity($requestId, $providerId, 0);
927            if ($requestRelation) {
928                $maxQuantity = $requestRelation->getMaxQuantity();
929                if ($maxQuantity !== null && $maxQuantity > 0 && $count > (int) $maxQuantity) {
930                    return false;
931                }
932            }
933        }
934
935        return true;
936    }
937
938    protected function isMailWhitelisted(string $email, ScopeEntity $scope): bool
939    {
940        $emailsWithNoLimit = explode(',', $scope->getWhitelistedMails());
941        if (empty($emailsWithNoLimit)) {
942            return false;
943        }
944
945        foreach ($emailsWithNoLimit as $whitelistedMail) {
946            $whitelistedMail = trim($whitelistedMail);
947            if ($email === $whitelistedMail) {
948                return true;
949            }
950
951            if (str_starts_with($whitelistedMail, '@') && str_contains($email, $whitelistedMail)) {
952                return true;
953            }
954        }
955
956        return false;
957    }
958
959    public function readProcessWithSameDayAndDisplayNumber($scopeId, $displayNumber, $date)
960    {
961        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
962        $query->addEntityMapping()
963            ->addResolvedReferences(1)
964            ->addConditionScopeId($scopeId)
965            ->addConditionDate($date)
966            ->addConditionDisplayNumber($displayNumber)
967            ->addLimit(1);
968
969        $process = $this->fetchOne($query, new Entity());
970
971        return $this->readResolvedReferences($process, 1);
972    }
973
974    public function readProcessListByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null, $resolveReferences = 0, $limit = 1000): Collection
975    {
976        $query = new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT);
977        $query
978            ->addResolvedReferences($resolveReferences)
979            ->addEntityMapping()
980            ->addConditionExternalUserId($externalUserId);
981        if (!is_null($filterId)) {
982            $query->addConditionProcessId($filterId);
983        }
984        if (!is_null($status)) {
985            $query->addConditionStatus($status);
986        }
987        $query
988            ->addLimit($limit);
989
990        $statement = $this->fetchStatement($query);
991        $processList = $this->readProcessesWithoutRequests($statement);
992        return $this->attachAllRequestsInOneQuery($processList, (int) $resolveReferences);
993    }
994
995    public function readAssignedWorkstationIdForUpdate(int $processId): ?int
996    {
997        $query = (new \BO\Zmsbackend\Process\Repository\Process(\BO\Zmsbackend\Query\Base::SELECT))->getLockAssignedWorkstationId();
998        $value = $this->fetchValue($query, ['processId' => $processId]);
999
1000        if ($value === false || $value === null) {
1001            return null;
1002        }
1003
1004        return (int) $value;
1005    }
1006}