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