Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.09% covered (warning)
82.09%
408 / 497
75.56% covered (warning)
75.56%
34 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Process
82.09% covered (warning)
82.09%
408 / 497
75.56% covered (warning)
75.56%
34 / 45
232.06
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%
10 / 10
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%
10 / 10
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
 readNotificationReminderProcessList
100.00% covered (success)
100.00%
12 / 12
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
 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
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 authKey by processId
295     *
296     * @param
297     * processId
298     *
299     * @return String authKey
300     */
301    public function readAuthKeyByProcessId($processId)
302    {
303        $query = new Query\Process(Query\Base::SELECT);
304        $query
305            ->addEntityMapping()
306            ->addResolvedReferences(0)
307            ->addConditionProcessId($processId);
308        $process = $this->fetchOne($query, new Entity());
309        return ($process->hasId()) ? array(
310            'authName' => $process->getFirstClient()['familyName'],
311            'authKey' => $process->authKey
312        ) : null;
313    }
314
315    protected function readList($statement, $resolveReferences)
316    {
317        $query = new Query\Process(Query\Base::SELECT);
318        $processList = new Collection();
319        while ($processData = $statement->fetch(\PDO::FETCH_ASSOC)) {
320            $entity = new Entity($query->postProcessJoins($processData));
321            $entity = $this->readResolvedReferences($entity, $resolveReferences);
322            $processList->addEntity($entity);
323        }
324        return $processList;
325    }
326
327    /**
328     * Read list with following processes in DB
329     */
330    public function readEntityList($processId, $resolveReferences = 0)
331    {
332        $query = new Query\Process(Query\Base::SELECT);
333        $query
334            ->addResolvedReferences($resolveReferences)
335            ->addEntityMapping()
336            ->addConditionProcessIdFollow($processId);
337        $statement = $this->fetchStatement($query);
338        return $this->readList($statement, $resolveReferences)->sortByAppointmentDate();
339    }
340
341    public function readByWorkstation(\BO\Zmsentities\Workstation $workstation, $resolveReferences = 0)
342    {
343        $query = new Query\Process(Query\Base::SELECT);
344        $query
345            ->addResolvedReferences($resolveReferences)
346            ->addEntityMapping()
347            ->addConditionWorkstationId($workstation->id);
348        $process = $this->fetchOne($query, new Entity());
349        return ($process->hasId()) ? $this->readResolvedReferences($process, $resolveReferences) : null;
350    }
351
352    /**
353     * Read processList by scopeId and DateTime
354     *
355     * @param
356     * scopeId
357     * dateTime
358     *
359     * @return Collection processList
360     */
361    public function readProcessListByScopesAndTime(
362        $scopeIds,
363        \DateTimeInterface $dateTime,
364        $resolveReferences = 0,
365        $withEntities = []
366    ) {
367        $query = new Query\Process(Query\Base::SELECT, '', false, null, $withEntities);
368        $query
369            ->addResolvedReferences($resolveReferences)
370            ->addEntityMapping()
371            ->addConditionScopeIds($scopeIds)
372            ->addConditionAssigned()
373            ->addConditionIgnoreSlots()
374            ->addConditionTime($dateTime)
375            ->removeDuplicates();
376        $statement = $this->fetchStatement($query);
377        return $this->readList($statement, $resolveReferences);
378    }
379
380    /**
381     * Read conflictList by scopeId and DateTime
382     *
383     * @param
384     * scopeId
385     * dateTime
386     *
387     * @return Collection processList
388     */
389    public function readConflictListByScopeAndTime(\BO\Zmsentities\Scope $scope, \DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null, \DateTimeInterface $now = null, $resolveReferences = 1)
390    {
391        $availabilityList = (new Availability())
392            ->readAvailabilityListByScope($scope, 0, $startDate, $endDate)
393            ->withScope($scope);
394        if (! $endDate) {
395            $availabilityList = $availabilityList->withDateTime($startDate);
396            $endDate = $startDate;
397        }
398        $currentDate = ($startDate) ? $startDate : $now;
399        $conflictList = $availabilityList->getConflicts($startDate, $endDate);
400        while ($currentDate <= $endDate) {
401            $query = new Query\Process(Query\Base::SELECT);
402            $query
403                ->addResolvedReferences($resolveReferences)
404                ->addEntityMapping()
405                ->addConditionScopeId($scope->getId())
406                ->addConditionAssigned()
407                ->addConditionIgnoreSlots();
408            $query->addConditionTime($currentDate);
409            $statement = $this->fetchStatement($query);
410            $processList = $this->readList($statement, $resolveReferences);
411            $processList = $processList->toQueueList($currentDate)->withoutStatus(['queued'])->toProcessList();
412            $conflictList->addList($processList->withOutAvailability($availabilityList));
413            $currentDate = $currentDate->modify('+1 day');
414        }
415        $conflictList = $conflictList->withoutExpiredAppointmentDate($now);
416        return $conflictList;
417    }
418
419
420    /**
421     * Read processList by scopeId and status
422     *
423     * @return Collection processList
424     */
425    public function readProcessListByScopeAndStatus($scopeId, $status, $resolveReferences = 0, $limit = 1000, $offset = null)
426    {
427        $query = new Query\Process(Query\Base::SELECT);
428        $query
429            ->addResolvedReferences($resolveReferences)
430            ->addEntityMapping()
431            ->addConditionScopeId($scopeId) //removed because of dismatching scope and pickup scope
432            ->addConditionStatus($status)
433            ->addLimit($limit, $offset);
434        $statement = $this->fetchStatement($query);
435        return $this->readList($statement, $resolveReferences);
436    }
437
438    public function readSearch(array $parameter, $resolveReferences = 0, $limit = 100)
439    {
440        $query = new Query\Process(Query\Base::SELECT);
441        $query
442            ->addResolvedReferences($resolveReferences)
443            ->addEntityMapping()
444            ->addConditionAssigned()
445            ->addConditionIgnoreSlots()
446            ->addLimit($limit);
447        if (isset($parameter['query'])) {
448            if (preg_match('#^\d+$#', $parameter['query'])) {
449                $query->addConditionProcessId($parameter['query']);
450                $query->addConditionSearch($parameter['query'], true);
451            } else {
452                $query->addConditionSearch($parameter['query']);
453            }
454            unset($parameter['query']);
455        }
456        if (count($parameter)) {
457            $query = $this->addSearchConditions($query, $parameter);
458        }
459
460        $statement = $this->fetchStatement($query);
461        return $this->readList($statement, $resolveReferences);
462    }
463
464    protected function addSearchConditions(Query\Process $query, $parameter)
465    {
466        if (isset($parameter['processId']) && $parameter['processId']) {
467            $query->addConditionProcessId($parameter['processId']);
468        }
469        if (isset($parameter['name']) && $parameter['name']) {
470            $exact = (isset($parameter['exact'])) ? $parameter['exact'] : false;
471            $query->addConditionName($parameter['name'], $exact);
472        }
473        if (isset($parameter['amendment']) && $parameter['amendment']) {
474            $query->addConditionAmendment($parameter['amendment']);
475        }
476        if (isset($parameter['scopeId']) && $parameter['scopeId']) {
477            $query->addConditionScopeId($parameter['scopeId']);
478        }
479        if (isset($parameter['authKey']) && $parameter['authKey']) {
480            $query->addConditionAuthKey($parameter['authKey']);
481        }
482        if (isset($parameter['requestId']) && $parameter['requestId']) {
483            $query->addConditionRequestId($parameter['requestId']);
484        }
485        return $query;
486    }
487
488    /**
489     * Read processList by clusterId and DateTime
490     *
491     * @param
492     * clusterId
493     * dateTime
494     *
495     * @return Collection processList
496     */
497    public function readProcessListByClusterAndTime($clusterId, \DateTimeInterface $dateTime)
498    {
499        $cluster = (new Cluster())->readEntity($clusterId, 1);
500        $scopeIds = [];
501        if ($cluster->scopes->count()) {
502            foreach ($cluster->scopes as $scope) {
503                $scopeIds[] = $scope->id;
504            }
505        }
506
507        return $this->readProcessListByScopesAndTime($scopeIds, $dateTime);
508    }
509
510    /**
511     * Read processList by scopeId to get a number of all processes of a scope
512     *
513     * @param
514     * scopeId
515     *
516     * @return Collection processList
517     */
518    public function readProcessListCountByScope($scopeId)
519    {
520        $query = new Query\Process(Query\Base::SELECT);
521        $query
522            ->addCountValue()
523            ->addConditionAssigned()
524            ->addConditionIgnoreSlots()
525            ->addConditionScopeId($scopeId);
526        $statement = $this->fetchStatement($query);
527        $result = $statement->fetch(\PDO::FETCH_ASSOC);
528        return $result['processCount'];
529    }
530
531    /**
532     * Read processList by mail address
533     *
534     * @return Collection processList
535     */
536    public function readProcessListByMailAddress(string $mailAddress, int $scopeId = null, $resolveReferences = 0, $limit = 2000): Collection
537    {
538        $query = new Query\Process(Query\Base::SELECT);
539        $query
540            ->addResolvedReferences($resolveReferences)
541            ->addEntityMapping()
542            ->addConditionMail($mailAddress, true)
543            ->addConditionIgnoreSlots()
544            ->addLimit($limit);
545        if ($scopeId) {
546            $query->addConditionScopeId($scopeId);
547        }
548
549        $statement = $this->fetchStatement($query);
550        return $this->readList($statement, $resolveReferences);
551    }
552
553    /**
554     * Read processList by mail address and statuslist
555     *
556     * @return Collection processList
557     */
558    public function readListByMailAndStatusList(string $mailAddress, array $statusList, $resolveReferences = 1, $limit = 300): Collection
559    {
560        $query = new Query\Process(Query\Base::SELECT);
561        $query
562            ->addResolvedReferences($resolveReferences)
563            ->addEntityMapping()
564            ->addConditionMail($mailAddress, true)
565            ->addConditionIgnoreSlots()
566            ->addLimit($limit);
567        $statement = $this->fetchStatement($query);
568        $collection = $this->readList($statement, $resolveReferences);
569        return $collection->toProcessListByStatusList($statusList);
570    }
571
572
573    /**
574     * Markiere einen Termin als bestätigt
575     *
576     * @param
577     * process
578     *
579     * @return Resource Status
580     */
581    public function updateProcessStatus(Entity $process, $status, \DateTimeInterface $dateTime, $resolveReferences = 0, $userAccount = null)
582    {
583        $process = (new ProcessStatus())
584            ->writeUpdatedStatus($process, $status, $dateTime, $resolveReferences, $userAccount);
585
586        /** @var Entity $process */
587        if ($this->shouldUpdateDisplayNumber($process, $status)) {
588            $this->updateEntityDisplayNumber($process);
589        }
590
591        return $process;
592    }
593
594    public function shouldUpdateDisplayNumber(Entity $process, $status): bool
595    {
596        if ($status !== 'preconfirmed' && $status !== 'confirmed') {
597            return false;
598        }
599        $displayNumberPrefix = $process->scope->getPreference('queue', 'displayNumberPrefix');
600        if (empty($displayNumberPrefix)) {
601            return false;
602        }
603
604        if (str_starts_with($process->getDisplayNumber(), $displayNumberPrefix)) {
605            return false;
606        }
607
608        return true;
609    }
610
611    /**
612     * Löscht einen Termin aus der Datenbank
613     * Regulär sollte aber ProcessStatusArchived::writeEntityFinished()
614     * oder self::writeBlockedEntity() verwendet werden.
615     *
616     * @return Resource Status
617     */
618    public function writeDeletedEntity($processId): bool
619    {
620        $processEntityList = $this->readEntityList($processId);
621        $status = false;
622        if ($processEntityList->count()) {
623            foreach ($processEntityList as $entity) {
624                $entityId = $entity->getId();
625                $query = Query\Process::QUERY_DELETE;
626                $status = $this->perform($query, array(
627                    $entityId,
628                    $entityId
629                ));
630                if ($status) {
631                            $this->deleteRequestsForProcessId($entityId);
632                            (new Slot())->deleteSlotProcessMappingFor($entityId);
633                            Log::writeProcessLog("DELETE (Process::writeDeletedEntity) $entityId ", Log::ACTION_DELETED, $entity);
634                }
635            }
636        }
637
638        return $status;
639    }
640
641    /**
642     * ACHTUNG: Nur noch als Helferfunction vor Refactoring durch MF,
643     * damit unittests und zmsappointment wie gewohnt funktionieren
644     *
645     * @param
646     *            processId and authKey
647     *
648     * @return Resource Status
649     */
650    public function deleteEntity($processId, $authKey)
651    {
652        return $this->writeCanceledEntity($processId, $authKey);
653    }
654
655    /**
656     * Markiere einen Termin als abgesagt
657     *
658     * @param
659     *            processId and authKey
660     *
661     * @return Resource Status
662     */
663    public function writeCanceledEntity($processId, $authKey, $now = null, ?\BO\Zmsentities\Useraccount $useraccount = null)
664    {
665        $canceledTimestamp = ($now) ? $now->getTimestamp() : (new \DateTimeImmutable())->getTimestamp();
666        $query = Query\Process::QUERY_CANCELED;
667        $this->perform($query, [
668            'processId' => $processId,
669            'authKey' => $authKey,
670            'canceledTimestamp' => $canceledTimestamp
671        ]);
672        $process = $this->readEntity($processId, new Helper\NoAuth(), 0);
673        Log::writeProcessLog("DELETE (Process::writeCanceledEntity) $processId ", Log::ACTION_CANCELED, $process, $useraccount);
674        return $process;
675    }
676
677    /**
678     * Markiere einen Termin als dereferenced
679     *
680     * @param
681     *            processId and authKey
682     *
683     * @return Resource Status
684     */
685    public function writeBlockedEntity(\BO\Zmsentities\Process $process, bool $releaseSlots = false, ?\BO\Zmsentities\Useraccount $useraccount = null)
686    {
687        $amendment = $process->toDerefencedAmendment();
688        $customTextfield = $process->toDerefencedcustomTextfield();
689        $customTextfield2 = $process->toDerefencedcustomTextfield2();
690        if (!isset($process->queue['status'])) {
691            $process->queue['status'] = $process->status;
692        }
693        $process->status = 'blocked';
694        $query = Query\Process::QUERY_DEREFERENCED;
695        $status = $this->perform($query, array(
696            $amendment,
697            $customTextfield,
698            $customTextfield2,
699            $process->id,
700            $process->authKey,
701            $process->id
702        ));
703        if ($status) {
704            $processEntityList = $this->readEntityList($process->id);
705            if ($processEntityList->count()) {
706                foreach ($processEntityList as $entity) {
707                    $entityId = $entity->getId();
708                    if ($releaseSlots) {
709                        (new Slot())->deleteSlotProcessMappingFor($entityId);
710                    }
711                    Log::writeProcessLog("DELETE (Process::writeBlockedEntity) $entityId ", Log::ACTION_DELETED, $process, $useraccount);
712                }
713            }
714        }
715        return $status;
716    }
717
718    protected function writeRequestsToDb(\BO\Zmsentities\Process $process)
719    {
720        // Beware of resolveReferences=0 to not delete the existing requests, except for queued processes
721        $hasRequests = ($process->requests && count($process->requests));
722        if ($hasRequests || 'queued' == $process->status) {
723            $this->deleteRequestsForProcessId($process->id);
724        }
725        if ($hasRequests) {
726            $query = new Query\XRequest(Query\Base::INSERT);
727            foreach ($process->requests as $request) {
728                if ($request->id >= 0) {
729        // allow deleting requests with a -1 request
730                    $query->addValues([
731                            'AnliegenID' => $request['id'],
732                            'source' => $request['source'],
733                            'BuergerID' => $process->id
734                        ]);
735                    $this->writeItem($query);
736                }
737            }
738        }
739    }
740
741    protected function deleteRequestsForProcessId($processId)
742    {
743        $status = false;
744        if (0 < $processId) {
745            $query = new Query\XRequest(Query\Base::DELETE);
746            $query->addConditionProcessId($processId);
747            $status = $this->deleteItem($query);
748        }
749        return $status;
750    }
751
752    public function readExpiredProcessList(\DateTimeInterface $expirationDate, $limit = 500, $resolveReferences = 0, $offset = null)
753    {
754        $selectQuery = new Query\Process(Query\Base::SELECT);
755        $selectQuery
756            ->addEntityMapping()
757            ->addResolvedReferences($resolveReferences)
758            ->addConditionProcessDeleteInterval($expirationDate)
759            ->addConditionIgnoreSlots()
760            ->addLimit($limit, $offset);
761        $statement = $this->fetchStatement($selectQuery);
762        return $this->readList($statement, $resolveReferences);
763    }
764    public function readUnconfirmedProcessList(\DateTimeInterface $expirationDate, $scopeId = 0, $limit = 500, $offset = null, $resolveReferences = 0)
765    {
766
767        $selectQuery = new Query\Process(Query\Base::SELECT);
768        $selectQuery
769            ->addEntityMapping()
770            ->addResolvedReferences($resolveReferences)
771            ->addConditionScopeId($scopeId)
772            ->addConditionProcessExpiredIPTimeStamp($expirationDate)
773            ->addConditionStatus('preconfirmed')
774            ->addConditionIgnoreSlots()
775            ->addLimit($limit, $offset);
776        $statement = $this->fetchStatement($selectQuery);
777        return $this->readList($statement, $resolveReferences);
778    }
779
780    public function readExpiredProcessListByStatus(\DateTimeInterface $expirationDate, $status, $limit = 500, $offset = null, $resolveReferences = 0)
781    {
782        $selectQuery = new Query\Process(Query\Base::SELECT);
783        $selectQuery
784            ->addEntityMapping()
785            ->addResolvedReferences($resolveReferences)
786            ->addConditionProcessDeleteInterval($expirationDate)
787            ->addConditionStatus($status)
788            ->addConditionIgnoreSlots()
789            ->addLimit($limit, $offset);
790        $statement = $this->fetchStatement($selectQuery);
791        return $this->readList($statement, $resolveReferences);
792    }
793
794    public function readExpiredReservationsList(\DateTimeInterface $expirationDate, $scopeId, $limit = 500, $offset = null, $resolveReferences = 0)
795    {
796        $selectQuery = new Query\Process(Query\Base::SELECT);
797        $selectQuery
798            ->addEntityMapping()
799            ->addResolvedReferences($resolveReferences)
800            ->addConditionScopeId($scopeId)
801            ->addConditionIsReserved()
802            ->addConditionProcessExpiredIPTimeStamp($expirationDate)
803            ->addConditionIgnoreSlots()
804            ->addLimit($limit, $offset);
805        $statement = $this->fetchStatement($selectQuery);
806        return $this->readList($statement, $resolveReferences);
807    }
808
809    public function readNotificationReminderProcessList(\DateTimeInterface $dateTime, $limit = 500, $offset = null, $resolveReferences = 0)
810    {
811        $selectQuery = new Query\Process(Query\Base::SELECT);
812        $selectQuery
813            ->addEntityMapping()
814            ->addResolvedReferences($resolveReferences)
815            ->addConditionProcessReminderInterval($dateTime)
816            ->addConditionHasTelephone()
817            ->addConditionAssigned()
818            ->addConditionIgnoreSlots()
819            ->addConditionStatus('confirmed')
820            ->addLimit($limit, $offset);
821        $statement = $this->fetchStatement($selectQuery);
822        return $this->readList($statement, $resolveReferences)->withDepartmentNotificationEnabled();
823    }
824
825    public function readEmailReminderProcessListByInterval(\DateTimeInterface $now, \DateTimeInterface $lastRun, $defaultReminderInMinutes, $limit = 500, $offset = null, $resolveReferences = 0)
826    {
827        $selectQuery = new Query\Process(Query\Base::SELECT);
828        $selectQuery
829            ->addEntityMapping()
830            ->addResolvedReferences($resolveReferences)
831            ->addConditionProcessMailReminder($now, $lastRun, $defaultReminderInMinutes)
832            ->addConditionAssigned()
833            ->addConditionIgnoreSlots()
834            ->addConditionStatus('confirmed')
835            ->addLimit($limit, $offset);
836        $statement = $this->fetchStatement($selectQuery);
837        return $this->readList($statement, $resolveReferences)->withDepartmentHasMailFrom();
838    }
839
840    public function readDeallocateProcessList(\DateTimeInterface $now, $limit = 500, $offset = null, $resolveReferences = 0)
841    {
842        $selectQuery = new Query\Process(Query\Base::SELECT);
843        $selectQuery
844            ->addEntityMapping()
845            ->addResolvedReferences($resolveReferences)
846            ->addConditionDeallocate($now)
847            ->addConditionIgnoreSlots()
848            ->addLimit($limit, $offset);
849        $statement = $this->fetchStatement($selectQuery);
850        return $this->readList($statement, $resolveReferences);
851    }
852
853    public function isAppointmentAllowedWithSameMail(Entity $entity): bool
854    {
855        if (empty($entity->getClients()) || empty($entity->getClients()->getFirst())) {
856            return true;
857        }
858
859        $maxAppointmentsPerMail = $entity->scope->getAppointmentsPerMail();
860        $emailToCheck = $entity->getClients()->getFirst()->email;
861        if ($maxAppointmentsPerMail < 1 || empty($emailToCheck)) {
862            return true;
863        }
864
865        if ($this->isMailWhitelisted($emailToCheck, $entity->scope)) {
866            return true;
867        }
868
869        $processes = $this->readProcessListByMailAddress($entity->getClients()->getFirst()->email, $entity->scope->id);
870        $activeAppointments = 0;
871        foreach ($processes as $process) {
872            if ($entity->id == $process->id) {
873                return true;
874            }
875
876            if (in_array($process->getStatus(), ['preconfirmed', 'confirmed'])) {
877                $activeAppointments++;
878                if ($activeAppointments >= $maxAppointmentsPerMail) {
879                    return false;
880                }
881            }
882        }
883
884        return true;
885    }
886
887    protected function isMailWhitelisted(string $email, ScopeEntity $scope): bool
888    {
889        $emailsWithNoLimit = explode(',', $scope->getWhitelistedMails());
890        if (empty($emailsWithNoLimit)) {
891            return false;
892        }
893
894        foreach ($emailsWithNoLimit as $whitelistedMail) {
895            $whitelistedMail = trim($whitelistedMail);
896            if ($email === $whitelistedMail) {
897                return true;
898            }
899
900            if (str_starts_with($whitelistedMail, '@') && str_contains($email, $whitelistedMail)) {
901                return true;
902            }
903        }
904
905        return false;
906    }
907
908    public function readProcessWithSameDayAndDisplayNumber($scopeId, $displayNumber, $date)
909    {
910        $query = new Query\Process(Query\Base::SELECT);
911        $query->addEntityMapping()
912            ->addResolvedReferences(1)
913            ->addConditionScopeId($scopeId)
914            ->addConditionDate($date)
915            ->addConditionDisplayNumber($displayNumber)
916            ->addLimit(1);
917
918        $process = $this->fetchOne($query, new Entity());
919
920        return $this->readResolvedReferences($process, 1);
921    }
922
923    /**
924     * Read processList by external user id
925     *
926     * @return Collection processList
927     */
928    public function readProcessListByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null, $resolveReferences = 0, $limit = 1000): Collection
929    {
930        $query = new Query\Process(Query\Base::SELECT);
931        $query
932            ->addResolvedReferences($resolveReferences)
933            ->addEntityMapping()
934            ->addConditionExternalUserId($externalUserId);
935        if (!is_null($filterId)) {
936            $query->addConditionProcessId($filterId);
937        }
938        if (!is_null($status)) {
939            $query->addConditionStatus($status);
940        }
941        $query
942            ->addLimit($limit);
943
944        $statement = $this->fetchStatement($query);
945        return $this->readList($statement, $resolveReferences);
946    }
947}