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