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