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