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