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