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