Code Coverage  | 
      ||||||||||
Lines  | 
       Functions and Methods  | 
       Classes and Traits  | 
      ||||||||
| Total |         | 
       81.93%  | 
       417 / 509  | 
               | 
       76.09%  | 
       35 / 46  | 
       CRAP |         | 
       0.00%  | 
       0 / 1  | 
      
| Process |         | 
       81.93%  | 
       417 / 509  | 
               | 
       76.09%  | 
       35 / 46  | 
       237.45 |         | 
       0.00%  | 
       0 / 1  | 
      
| readEntity |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| readById |         | 
       0.00%  | 
       0 / 5  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| readResolvedReferences |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       3 | |||
| updateEntity |         | 
       100.00%  | 
       16 / 16  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| updateEntityDisplayNumber |         | 
       0.00%  | 
       0 / 9  | 
               | 
       0.00%  | 
       0 / 1  | 
       6 | |||
| updateEntityWithSlots |         | 
       0.00%  | 
       0 / 19  | 
               | 
       0.00%  | 
       0 / 1  | 
       42 | |||
| writeNewPickup |         | 
       100.00%  | 
       12 / 12  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| redirectToScope |         | 
       0.00%  | 
       0 / 14  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| readSlotCount |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| writeEntityWithNewAppointment |         | 
       100.00%  | 
       18 / 18  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| updateFollowingProcesses |         | 
       100.00%  | 
       13 / 13  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| updateReassignedRequests |         | 
       100.00%  | 
       4 / 4  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| writeNewProcess |         | 
       100.00%  | 
       20 / 20  | 
               | 
       100.00%  | 
       1 / 1  | 
       5 | |||
| readNewProcessId |         | 
       100.00%  | 
       3 / 3  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readAuthKeyByProcessId |         | 
       100.00%  | 
       10 / 10  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| readList |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| readEntityList |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readByWorkstation |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| readProcessListByScopeAndTime |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readConflictListByScopeAndTime |         | 
       100.00%  | 
       24 / 24  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| readProcessListByScopeAndStatus |         | 
       100.00%  | 
       9 / 9  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readSearch |         | 
       100.00%  | 
       17 / 17  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| addSearchConditions |         | 
       100.00%  | 
       14 / 14  | 
               | 
       100.00%  | 
       1 / 1  | 
       14 | |||
| readProcessListByClusterAndTime |         | 
       100.00%  | 
       6 / 6  | 
               | 
       100.00%  | 
       1 / 1  | 
       3 | |||
| readProcessListCountByScope |         | 
       100.00%  | 
       9 / 9  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readProcessListByMailAddress |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| readListByMailAndStatusList |         | 
       100.00%  | 
       10 / 10  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| updateProcessStatus |         | 
       80.00%  | 
       4 / 5  | 
               | 
       0.00%  | 
       0 / 1  | 
       2.03 | |||
| shouldUpdateDisplayNumber |         | 
       25.00%  | 
       2 / 8  | 
               | 
       0.00%  | 
       0 / 1  | 
       10.75 | |||
| writeDeletedEntity |         | 
       100.00%  | 
       15 / 15  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| deleteEntity |         | 
       100.00%  | 
       1 / 1  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| writeCanceledEntity |         | 
       100.00%  | 
       10 / 10  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| writeBlockedEntity |         | 
       100.00%  | 
       24 / 24  | 
               | 
       100.00%  | 
       1 / 1  | 
       6 | |||
| writeRequestsToDb |         | 
       100.00%  | 
       13 / 13  | 
               | 
       100.00%  | 
       1 / 1  | 
       7 | |||
| deleteRequestsForProcessId |         | 
       100.00%  | 
       6 / 6  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| readExpiredProcessList |         | 
       100.00%  | 
       9 / 9  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readUnconfirmedProcessList |         | 
       0.00%  | 
       0 / 11  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| readExpiredProcessListByStatus |         | 
       100.00%  | 
       10 / 10  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readExpiredReservationsList |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readNotificationReminderProcessList |         | 
       100.00%  | 
       12 / 12  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readEmailReminderProcessListByInterval |         | 
       100.00%  | 
       11 / 11  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| readDeallocateProcessList |         | 
       100.00%  | 
       9 / 9  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| isAppointmentAllowedWithSameMail |         | 
       83.33%  | 
       15 / 18  | 
               | 
       0.00%  | 
       0 / 1  | 
       10.46 | |||
| isMailWhitelisted |         | 
       80.00%  | 
       8 / 10  | 
               | 
       0.00%  | 
       0 / 1  | 
       6.29 | |||
| readProcessWithSameDayAndDisplayNumber |         | 
       0.00%  | 
       0 / 9  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| readProcessListByExternalUserId |         | 
       0.00%  | 
       0 / 13  | 
               | 
       0.00%  | 
       0 / 1  | 
       12 | |||
| 1 | <?php | 
| 2 | |
| 3 | namespace BO\Zmsdb; | 
| 4 | |
| 5 | use BO\Zmsentities\Collection\AppointmentList; | 
| 6 | use BO\Zmsentities\Process as Entity; | 
| 7 | use BO\Zmsentities\Scope as ScopeEntity; | 
| 8 | use BO\Zmsentities\Collection\ProcessList as Collection; | 
| 9 | use BO\Zmsdb\Helper\ProcessStatus; | 
| 10 | |
| 11 | /** | 
| 12 | * | 
| 13 | * @SuppressWarnings(CouplingBetweenObjects) | 
| 14 | * @SuppressWarnings(TooManyPublicMethods) | 
| 15 | * @SuppressWarnings(Complexity) | 
| 16 | * @SuppressWarnings(TooManyMethods) | 
| 17 | */ | 
| 18 | class 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 | 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($scopeId, \DateTimeInterface $dateTime, $resolveReferences = 0) | 
| 379 | { | 
| 380 | $query = new Query\Process(Query\Base::SELECT); | 
| 381 | $query | 
| 382 | ->addResolvedReferences($resolveReferences) | 
| 383 | ->addEntityMapping() | 
| 384 | ->addConditionScopeId($scopeId) | 
| 385 | ->addConditionAssigned() | 
| 386 | ->addConditionIgnoreSlots() | 
| 387 | ->addConditionTime($dateTime) | 
| 388 | ->removeDuplicates(); | 
| 389 | $statement = $this->fetchStatement($query); | 
| 390 | return $this->readList($statement, $resolveReferences); | 
| 391 | } | 
| 392 | |
| 393 | /** | 
| 394 | * Read conflictList by scopeId and DateTime | 
| 395 | * | 
| 396 | * @param | 
| 397 | * scopeId | 
| 398 | * dateTime | 
| 399 | * | 
| 400 | * @return Collection processList | 
| 401 | */ | 
| 402 | public function readConflictListByScopeAndTime(\BO\Zmsentities\Scope $scope, \DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null, \DateTimeInterface $now = null, $resolveReferences = 1) | 
| 403 | { | 
| 404 | $availabilityList = (new Availability()) | 
| 405 | ->readAvailabilityListByScope($scope, 0, $startDate, $endDate) | 
| 406 | ->withScope($scope); | 
| 407 | if (! $endDate) { | 
| 408 | $availabilityList = $availabilityList->withDateTime($startDate); | 
| 409 | $endDate = $startDate; | 
| 410 | } | 
| 411 | $currentDate = ($startDate) ? $startDate : $now; | 
| 412 | $conflictList = $availabilityList->getConflicts($startDate, $endDate); | 
| 413 | while ($currentDate <= $endDate) { | 
| 414 | $query = new Query\Process(Query\Base::SELECT); | 
| 415 | $query | 
| 416 | ->addResolvedReferences($resolveReferences) | 
| 417 | ->addEntityMapping() | 
| 418 | ->addConditionScopeId($scope->getId()) | 
| 419 | ->addConditionAssigned() | 
| 420 | ->addConditionIgnoreSlots(); | 
| 421 | $query->addConditionTime($currentDate); | 
| 422 | $statement = $this->fetchStatement($query); | 
| 423 | $processList = $this->readList($statement, $resolveReferences); | 
| 424 | $processList = $processList->toQueueList($currentDate)->withoutStatus(['queued'])->toProcessList(); | 
| 425 | $conflictList->addList($processList->withOutAvailability($availabilityList)); | 
| 426 | $currentDate = $currentDate->modify('+1 day'); | 
| 427 | } | 
| 428 | $conflictList = $conflictList->withoutExpiredAppointmentDate($now); | 
| 429 | return $conflictList; | 
| 430 | } | 
| 431 | |
| 432 | |
| 433 | /** | 
| 434 | * Read processList by scopeId and status | 
| 435 | * | 
| 436 | * @return Collection processList | 
| 437 | */ | 
| 438 | public function readProcessListByScopeAndStatus($scopeId, $status, $resolveReferences = 0, $limit = 1000, $offset = null) | 
| 439 | { | 
| 440 | $query = new Query\Process(Query\Base::SELECT); | 
| 441 | $query | 
| 442 | ->addResolvedReferences($resolveReferences) | 
| 443 | ->addEntityMapping() | 
| 444 | ->addConditionScopeId($scopeId) //removed because of dismatching scope and pickup scope | 
| 445 | ->addConditionStatus($status) | 
| 446 | ->addLimit($limit, $offset); | 
| 447 | $statement = $this->fetchStatement($query); | 
| 448 | return $this->readList($statement, $resolveReferences); | 
| 449 | } | 
| 450 | |
| 451 | public function readSearch(array $parameter, $resolveReferences = 0, $limit = 100) | 
| 452 | { | 
| 453 | $query = new Query\Process(Query\Base::SELECT); | 
| 454 | $query | 
| 455 | ->addResolvedReferences($resolveReferences) | 
| 456 | ->addEntityMapping() | 
| 457 | ->addConditionAssigned() | 
| 458 | ->addConditionIgnoreSlots() | 
| 459 | ->addLimit($limit); | 
| 460 | if (isset($parameter['query'])) { | 
| 461 | if (preg_match('#^\d+$#', $parameter['query'])) { | 
| 462 | $query->addConditionProcessId($parameter['query']); | 
| 463 | $query->addConditionSearch($parameter['query'], true); | 
| 464 | } else { | 
| 465 | $query->addConditionSearch($parameter['query']); | 
| 466 | } | 
| 467 | unset($parameter['query']); | 
| 468 | } | 
| 469 | if (count($parameter)) { | 
| 470 | $query = $this->addSearchConditions($query, $parameter); | 
| 471 | } | 
| 472 | |
| 473 | $statement = $this->fetchStatement($query); | 
| 474 | return $this->readList($statement, $resolveReferences); | 
| 475 | } | 
| 476 | |
| 477 | protected function addSearchConditions(Query\Process $query, $parameter) | 
| 478 | { | 
| 479 | if (isset($parameter['processId']) && $parameter['processId']) { | 
| 480 | $query->addConditionProcessId($parameter['processId']); | 
| 481 | } | 
| 482 | if (isset($parameter['name']) && $parameter['name']) { | 
| 483 | $exact = (isset($parameter['exact'])) ? $parameter['exact'] : false; | 
| 484 | $query->addConditionName($parameter['name'], $exact); | 
| 485 | } | 
| 486 | if (isset($parameter['amendment']) && $parameter['amendment']) { | 
| 487 | $query->addConditionAmendment($parameter['amendment']); | 
| 488 | } | 
| 489 | if (isset($parameter['scopeId']) && $parameter['scopeId']) { | 
| 490 | $query->addConditionScopeId($parameter['scopeId']); | 
| 491 | } | 
| 492 | if (isset($parameter['authKey']) && $parameter['authKey']) { | 
| 493 | $query->addConditionAuthKey($parameter['authKey']); | 
| 494 | } | 
| 495 | if (isset($parameter['requestId']) && $parameter['requestId']) { | 
| 496 | $query->addConditionRequestId($parameter['requestId']); | 
| 497 | } | 
| 498 | return $query; | 
| 499 | } | 
| 500 | |
| 501 | /** | 
| 502 | * Read processList by clusterId and DateTime | 
| 503 | * | 
| 504 | * @param | 
| 505 | * clusterId | 
| 506 | * dateTime | 
| 507 | * | 
| 508 | * @return Collection processList | 
| 509 | */ | 
| 510 | public function readProcessListByClusterAndTime($clusterId, \DateTimeInterface $dateTime) | 
| 511 | { | 
| 512 | $processList = new Collection(); | 
| 513 | $cluster = (new Cluster())->readEntity($clusterId, 1); | 
| 514 | if ($cluster->scopes->count()) { | 
| 515 | foreach ($cluster->scopes as $scope) { | 
| 516 | $processList->addList($this->readProcessListByScopeAndTime($scope->id, $dateTime)); | 
| 517 | } | 
| 518 | } | 
| 519 | return $processList; | 
| 520 | } | 
| 521 | |
| 522 | /** | 
| 523 | * Read processList by scopeId to get a number of all processes of a scope | 
| 524 | * | 
| 525 | * @param | 
| 526 | * scopeId | 
| 527 | * | 
| 528 | * @return Collection processList | 
| 529 | */ | 
| 530 | public function readProcessListCountByScope($scopeId) | 
| 531 | { | 
| 532 | $query = new Query\Process(Query\Base::SELECT); | 
| 533 | $query | 
| 534 | ->addCountValue() | 
| 535 | ->addConditionAssigned() | 
| 536 | ->addConditionIgnoreSlots() | 
| 537 | ->addConditionScopeId($scopeId); | 
| 538 | $statement = $this->fetchStatement($query); | 
| 539 | $result = $statement->fetch(\PDO::FETCH_ASSOC); | 
| 540 | return $result['processCount']; | 
| 541 | } | 
| 542 | |
| 543 | /** | 
| 544 | * Read processList by mail address | 
| 545 | * | 
| 546 | * @return Collection processList | 
| 547 | */ | 
| 548 | public function readProcessListByMailAddress(string $mailAddress, int $scopeId = null, $resolveReferences = 0, $limit = 2000): Collection | 
| 549 | { | 
| 550 | $query = new Query\Process(Query\Base::SELECT); | 
| 551 | $query | 
| 552 | ->addResolvedReferences($resolveReferences) | 
| 553 | ->addEntityMapping() | 
| 554 | ->addConditionMail($mailAddress, true) | 
| 555 | ->addConditionIgnoreSlots() | 
| 556 | ->addLimit($limit); | 
| 557 | if ($scopeId) { | 
| 558 | $query->addConditionScopeId($scopeId); | 
| 559 | } | 
| 560 | |
| 561 | $statement = $this->fetchStatement($query); | 
| 562 | return $this->readList($statement, $resolveReferences); | 
| 563 | } | 
| 564 | |
| 565 | /** | 
| 566 | * Read processList by mail address and statuslist | 
| 567 | * | 
| 568 | * @return Collection processList | 
| 569 | */ | 
| 570 | public function readListByMailAndStatusList(string $mailAddress, array $statusList, $resolveReferences = 1, $limit = 300): Collection | 
| 571 | { | 
| 572 | $query = new Query\Process(Query\Base::SELECT); | 
| 573 | $query | 
| 574 | ->addResolvedReferences($resolveReferences) | 
| 575 | ->addEntityMapping() | 
| 576 | ->addConditionMail($mailAddress, true) | 
| 577 | ->addConditionIgnoreSlots() | 
| 578 | ->addLimit($limit); | 
| 579 | $statement = $this->fetchStatement($query); | 
| 580 | $collection = $this->readList($statement, $resolveReferences); | 
| 581 | return $collection->toProcessListByStatusList($statusList); | 
| 582 | } | 
| 583 | |
| 584 | |
| 585 | /** | 
| 586 | * Markiere einen Termin als bestätigt | 
| 587 | * | 
| 588 | * @param | 
| 589 | * process | 
| 590 | * | 
| 591 | * @return Resource Status | 
| 592 | */ | 
| 593 | public function updateProcessStatus(Entity $process, $status, \DateTimeInterface $dateTime, $resolveReferences = 0, $userAccount = null) | 
| 594 | { | 
| 595 | $process = (new ProcessStatus()) | 
| 596 | ->writeUpdatedStatus($process, $status, $dateTime, $resolveReferences, $userAccount); | 
| 597 | |
| 598 | /** @var Entity $process */ | 
| 599 | if ($this->shouldUpdateDisplayNumber($process, $status)) { | 
| 600 | $this->updateEntityDisplayNumber($process); | 
| 601 | } | 
| 602 | |
| 603 | return $process; | 
| 604 | } | 
| 605 | |
| 606 | public function shouldUpdateDisplayNumber(Entity $process, $status): bool | 
| 607 | { | 
| 608 | if ($status !== 'preconfirmed') { | 
| 609 | return false; | 
| 610 | } | 
| 611 | |
| 612 | $displayNumberPrefix = $process->scope->getPreference('queue', 'displayNumberPrefix'); | 
| 613 | if (empty($displayNumberPrefix)) { | 
| 614 | return false; | 
| 615 | } | 
| 616 | |
| 617 | if (str_starts_with($process->getDisplayNumber(), $displayNumberPrefix)) { | 
| 618 | return false; | 
| 619 | } | 
| 620 | |
| 621 | return true; | 
| 622 | } | 
| 623 | |
| 624 | /** | 
| 625 | * Löscht einen Termin aus der Datenbank | 
| 626 | * Regulär sollte aber ProcessStatusArchived::writeEntityFinished() | 
| 627 | * oder self::writeBlockedEntity() verwendet werden. | 
| 628 | * | 
| 629 | * @return Resource Status | 
| 630 | */ | 
| 631 | public function writeDeletedEntity($processId): bool | 
| 632 | { | 
| 633 | $processEntityList = $this->readEntityList($processId); | 
| 634 | $status = false; | 
| 635 | if ($processEntityList->count()) { | 
| 636 | foreach ($processEntityList as $entity) { | 
| 637 | $entityId = $entity->getId(); | 
| 638 | $query = Query\Process::QUERY_DELETE; | 
| 639 | $status = $this->perform($query, array( | 
| 640 | $entityId, | 
| 641 | $entityId | 
| 642 | )); | 
| 643 | if ($status) { | 
| 644 | $this->deleteRequestsForProcessId($entityId); | 
| 645 | (new Slot())->deleteSlotProcessMappingFor($entityId); | 
| 646 | Log::writeProcessLog("DELETE (Process::writeDeletedEntity) $entityId ", Log::ACTION_DELETED, $entity); | 
| 647 | } | 
| 648 | } | 
| 649 | } | 
| 650 | |
| 651 | return $status; | 
| 652 | } | 
| 653 | |
| 654 | /** | 
| 655 | * ACHTUNG: Nur noch als Helferfunction vor Refactoring durch MF, | 
| 656 | * damit unittests und zmsappointment wie gewohnt funktionieren | 
| 657 | * | 
| 658 | * @param | 
| 659 | * processId and authKey | 
| 660 | * | 
| 661 | * @return Resource Status | 
| 662 | */ | 
| 663 | public function deleteEntity($processId, $authKey) | 
| 664 | { | 
| 665 | return $this->writeCanceledEntity($processId, $authKey); | 
| 666 | } | 
| 667 | |
| 668 | /** | 
| 669 | * Markiere einen Termin als abgesagt | 
| 670 | * | 
| 671 | * @param | 
| 672 | * processId and authKey | 
| 673 | * | 
| 674 | * @return Resource Status | 
| 675 | */ | 
| 676 | public function writeCanceledEntity($processId, $authKey, $now = null, ?\BO\Zmsentities\Useraccount $useraccount = null) | 
| 677 | { | 
| 678 | $canceledTimestamp = ($now) ? $now->getTimestamp() : (new \DateTimeImmutable())->getTimestamp(); | 
| 679 | $query = Query\Process::QUERY_CANCELED; | 
| 680 | $this->perform($query, [ | 
| 681 | 'processId' => $processId, | 
| 682 | 'authKey' => $authKey, | 
| 683 | 'canceledTimestamp' => $canceledTimestamp | 
| 684 | ]); | 
| 685 | $process = $this->readEntity($processId, new Helper\NoAuth(), 0); | 
| 686 | Log::writeProcessLog("DELETE (Process::writeCanceledEntity) $processId ", Log::ACTION_CANCELED, $process, $useraccount); | 
| 687 | return $process; | 
| 688 | } | 
| 689 | |
| 690 | /** | 
| 691 | * Markiere einen Termin als dereferenced | 
| 692 | * | 
| 693 | * @param | 
| 694 | * processId and authKey | 
| 695 | * | 
| 696 | * @return Resource Status | 
| 697 | */ | 
| 698 | public function writeBlockedEntity(\BO\Zmsentities\Process $process, bool $releaseSlots = false, ?\BO\Zmsentities\Useraccount $useraccount = null) | 
| 699 | { | 
| 700 | $amendment = $process->toDerefencedAmendment(); | 
| 701 | $customTextfield = $process->toDerefencedcustomTextfield(); | 
| 702 | $customTextfield2 = $process->toDerefencedcustomTextfield2(); | 
| 703 | if (!isset($process->queue['status'])) { | 
| 704 | $process->queue['status'] = $process->status; | 
| 705 | } | 
| 706 | $process->status = 'blocked'; | 
| 707 | $query = Query\Process::QUERY_DEREFERENCED; | 
| 708 | $status = $this->perform($query, array( | 
| 709 | $amendment, | 
| 710 | $customTextfield, | 
| 711 | $customTextfield2, | 
| 712 | $process->id, | 
| 713 | $process->authKey, | 
| 714 | $process->id | 
| 715 | )); | 
| 716 | if ($status) { | 
| 717 | $processEntityList = $this->readEntityList($process->id); | 
| 718 | if ($processEntityList->count()) { | 
| 719 | foreach ($processEntityList as $entity) { | 
| 720 | $entityId = $entity->getId(); | 
| 721 | if ($releaseSlots) { | 
| 722 | (new Slot())->deleteSlotProcessMappingFor($entityId); | 
| 723 | } | 
| 724 | Log::writeProcessLog("DELETE (Process::writeBlockedEntity) $entityId ", Log::ACTION_DELETED, $process, $useraccount); | 
| 725 | } | 
| 726 | } | 
| 727 | } | 
| 728 | return $status; | 
| 729 | } | 
| 730 | |
| 731 | protected function writeRequestsToDb(\BO\Zmsentities\Process $process) | 
| 732 | { | 
| 733 | // Beware of resolveReferences=0 to not delete the existing requests, except for queued processes | 
| 734 | $hasRequests = ($process->requests && count($process->requests)); | 
| 735 | if ($hasRequests || 'queued' == $process->status) { | 
| 736 | $this->deleteRequestsForProcessId($process->id); | 
| 737 | } | 
| 738 | if ($hasRequests) { | 
| 739 | $query = new Query\XRequest(Query\Base::INSERT); | 
| 740 | foreach ($process->requests as $request) { | 
| 741 | if ($request->id >= 0) { | 
| 742 | // allow deleting requests with a -1 request | 
| 743 | $query->addValues([ | 
| 744 | 'AnliegenID' => $request['id'], | 
| 745 | 'source' => $request['source'], | 
| 746 | 'BuergerID' => $process->id | 
| 747 | ]); | 
| 748 | $this->writeItem($query); | 
| 749 | } | 
| 750 | } | 
| 751 | } | 
| 752 | } | 
| 753 | |
| 754 | protected function deleteRequestsForProcessId($processId) | 
| 755 | { | 
| 756 | $status = false; | 
| 757 | if (0 < $processId) { | 
| 758 | $query = new Query\XRequest(Query\Base::DELETE); | 
| 759 | $query->addConditionProcessId($processId); | 
| 760 | $status = $this->deleteItem($query); | 
| 761 | } | 
| 762 | return $status; | 
| 763 | } | 
| 764 | |
| 765 | public function readExpiredProcessList(\DateTimeInterface $expirationDate, $limit = 500, $resolveReferences = 0, $offset = null) | 
| 766 | { | 
| 767 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 768 | $selectQuery | 
| 769 | ->addEntityMapping() | 
| 770 | ->addResolvedReferences($resolveReferences) | 
| 771 | ->addConditionProcessDeleteInterval($expirationDate) | 
| 772 | ->addConditionIgnoreSlots() | 
| 773 | ->addLimit($limit, $offset); | 
| 774 | $statement = $this->fetchStatement($selectQuery); | 
| 775 | return $this->readList($statement, $resolveReferences); | 
| 776 | } | 
| 777 | public function readUnconfirmedProcessList(\DateTimeInterface $expirationDate, $scopeId = 0, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 778 | { | 
| 779 | |
| 780 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 781 | $selectQuery | 
| 782 | ->addEntityMapping() | 
| 783 | ->addResolvedReferences($resolveReferences) | 
| 784 | ->addConditionScopeId($scopeId) | 
| 785 | ->addConditionProcessExpiredIPTimeStamp($expirationDate) | 
| 786 | ->addConditionStatus('preconfirmed') | 
| 787 | ->addConditionIgnoreSlots() | 
| 788 | ->addLimit($limit, $offset); | 
| 789 | $statement = $this->fetchStatement($selectQuery); | 
| 790 | return $this->readList($statement, $resolveReferences); | 
| 791 | } | 
| 792 | |
| 793 | public function readExpiredProcessListByStatus(\DateTimeInterface $expirationDate, $status, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 794 | { | 
| 795 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 796 | $selectQuery | 
| 797 | ->addEntityMapping() | 
| 798 | ->addResolvedReferences($resolveReferences) | 
| 799 | ->addConditionProcessDeleteInterval($expirationDate) | 
| 800 | ->addConditionStatus($status) | 
| 801 | ->addConditionIgnoreSlots() | 
| 802 | ->addLimit($limit, $offset); | 
| 803 | $statement = $this->fetchStatement($selectQuery); | 
| 804 | return $this->readList($statement, $resolveReferences); | 
| 805 | } | 
| 806 | |
| 807 | public function readExpiredReservationsList(\DateTimeInterface $expirationDate, $scopeId, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 808 | { | 
| 809 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 810 | $selectQuery | 
| 811 | ->addEntityMapping() | 
| 812 | ->addResolvedReferences($resolveReferences) | 
| 813 | ->addConditionScopeId($scopeId) | 
| 814 | ->addConditionIsReserved() | 
| 815 | ->addConditionProcessExpiredIPTimeStamp($expirationDate) | 
| 816 | ->addConditionIgnoreSlots() | 
| 817 | ->addLimit($limit, $offset); | 
| 818 | $statement = $this->fetchStatement($selectQuery); | 
| 819 | return $this->readList($statement, $resolveReferences); | 
| 820 | } | 
| 821 | |
| 822 | public function readNotificationReminderProcessList(\DateTimeInterface $dateTime, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 823 | { | 
| 824 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 825 | $selectQuery | 
| 826 | ->addEntityMapping() | 
| 827 | ->addResolvedReferences($resolveReferences) | 
| 828 | ->addConditionProcessReminderInterval($dateTime) | 
| 829 | ->addConditionHasTelephone() | 
| 830 | ->addConditionAssigned() | 
| 831 | ->addConditionIgnoreSlots() | 
| 832 | ->addConditionStatus('confirmed') | 
| 833 | ->addLimit($limit, $offset); | 
| 834 | $statement = $this->fetchStatement($selectQuery); | 
| 835 | return $this->readList($statement, $resolveReferences)->withDepartmentNotificationEnabled(); | 
| 836 | } | 
| 837 | |
| 838 | public function readEmailReminderProcessListByInterval(\DateTimeInterface $now, \DateTimeInterface $lastRun, $defaultReminderInMinutes, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 839 | { | 
| 840 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 841 | $selectQuery | 
| 842 | ->addEntityMapping() | 
| 843 | ->addResolvedReferences($resolveReferences) | 
| 844 | ->addConditionProcessMailReminder($now, $lastRun, $defaultReminderInMinutes) | 
| 845 | ->addConditionAssigned() | 
| 846 | ->addConditionIgnoreSlots() | 
| 847 | ->addConditionStatus('confirmed') | 
| 848 | ->addLimit($limit, $offset); | 
| 849 | $statement = $this->fetchStatement($selectQuery); | 
| 850 | return $this->readList($statement, $resolveReferences)->withDepartmentHasMailFrom(); | 
| 851 | } | 
| 852 | |
| 853 | public function readDeallocateProcessList(\DateTimeInterface $now, $limit = 500, $offset = null, $resolveReferences = 0) | 
| 854 | { | 
| 855 | $selectQuery = new Query\Process(Query\Base::SELECT); | 
| 856 | $selectQuery | 
| 857 | ->addEntityMapping() | 
| 858 | ->addResolvedReferences($resolveReferences) | 
| 859 | ->addConditionDeallocate($now) | 
| 860 | ->addConditionIgnoreSlots() | 
| 861 | ->addLimit($limit, $offset); | 
| 862 | $statement = $this->fetchStatement($selectQuery); | 
| 863 | return $this->readList($statement, $resolveReferences); | 
| 864 | } | 
| 865 | |
| 866 | public function isAppointmentAllowedWithSameMail(Entity $entity): bool | 
| 867 | { | 
| 868 | if (empty($entity->getClients()) || empty($entity->getClients()->getFirst())) { | 
| 869 | return true; | 
| 870 | } | 
| 871 | |
| 872 | $maxAppointmentsPerMail = $entity->scope->getAppointmentsPerMail(); | 
| 873 | $emailToCheck = $entity->getClients()->getFirst()->email; | 
| 874 | if ($maxAppointmentsPerMail < 1 || empty($emailToCheck)) { | 
| 875 | return true; | 
| 876 | } | 
| 877 | |
| 878 | if ($this->isMailWhitelisted($emailToCheck, $entity->scope)) { | 
| 879 | return true; | 
| 880 | } | 
| 881 | |
| 882 | $processes = $this->readProcessListByMailAddress($entity->getClients()->getFirst()->email, $entity->scope->id); | 
| 883 | $activeAppointments = 0; | 
| 884 | foreach ($processes as $process) { | 
| 885 | if ($entity->id == $process->id) { | 
| 886 | return true; | 
| 887 | } | 
| 888 | |
| 889 | if (in_array($process->getStatus(), ['preconfirmed', 'confirmed'])) { | 
| 890 | $activeAppointments++; | 
| 891 | if ($activeAppointments >= $maxAppointmentsPerMail) { | 
| 892 | return false; | 
| 893 | } | 
| 894 | } | 
| 895 | } | 
| 896 | |
| 897 | return true; | 
| 898 | } | 
| 899 | |
| 900 | protected function isMailWhitelisted(string $email, ScopeEntity $scope): bool | 
| 901 | { | 
| 902 | $emailsWithNoLimit = explode(',', $scope->getWhitelistedMails()); | 
| 903 | if (empty($emailsWithNoLimit)) { | 
| 904 | return false; | 
| 905 | } | 
| 906 | |
| 907 | foreach ($emailsWithNoLimit as $whitelistedMail) { | 
| 908 | $whitelistedMail = trim($whitelistedMail); | 
| 909 | if ($email === $whitelistedMail) { | 
| 910 | return true; | 
| 911 | } | 
| 912 | |
| 913 | if (str_starts_with($whitelistedMail, '@') && str_contains($email, $whitelistedMail)) { | 
| 914 | return true; | 
| 915 | } | 
| 916 | } | 
| 917 | |
| 918 | return false; | 
| 919 | } | 
| 920 | |
| 921 | public function readProcessWithSameDayAndDisplayNumber($scopeId, $displayNumber, $date) | 
| 922 | { | 
| 923 | $query = new Query\Process(Query\Base::SELECT); | 
| 924 | $query->addEntityMapping() | 
| 925 | ->addResolvedReferences(1) | 
| 926 | ->addConditionScopeId($scopeId) | 
| 927 | ->addConditionDate($date) | 
| 928 | ->addConditionDisplayNumber($displayNumber) | 
| 929 | ->addLimit(1); | 
| 930 | |
| 931 | $process = $this->fetchOne($query, new Entity()); | 
| 932 | |
| 933 | return $this->readResolvedReferences($process, 1); | 
| 934 | } | 
| 935 | |
| 936 | /** | 
| 937 | * Read processList by external user id | 
| 938 | * | 
| 939 | * @return Collection processList | 
| 940 | */ | 
| 941 | public function readProcessListByExternalUserId(string $externalUserId, ?int $filterId = null, ?string $status = null, $resolveReferences = 0, $limit = 1000): Collection | 
| 942 | { | 
| 943 | $query = new Query\Process(Query\Base::SELECT); | 
| 944 | $query | 
| 945 | ->addResolvedReferences($resolveReferences) | 
| 946 | ->addEntityMapping() | 
| 947 | ->addConditionExternalUserId($externalUserId); | 
| 948 | if (!is_null($filterId)) { | 
| 949 | $query->addConditionProcessId($filterId); | 
| 950 | } | 
| 951 | if (!is_null($status)) { | 
| 952 | $query->addConditionStatus($status); | 
| 953 | } | 
| 954 | $query | 
| 955 | ->addLimit($limit); | 
| 956 | |
| 957 | $statement = $this->fetchStatement($query); | 
| 958 | return $this->readList($statement, $resolveReferences); | 
| 959 | } | 
| 960 | } |