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