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