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