Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.07% covered (warning)
61.07%
80 / 131
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProcessStatusFree
61.07% covered (warning)
61.07%
80 / 131
55.56% covered (warning)
55.56%
5 / 9
92.42
0.00% covered (danger)
0.00%
0 / 1
 prepareCalendarAndDays
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getProcessDataHandle
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 readFreeProcesses
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 readFreeProcessesMinimalDeduplicated
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 extractProcessInfo
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 generateUniqueKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createMinimalProcess
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 readReservedProcesses
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 writeEntityReserved
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace BO\Zmsdb;
4
5use BO\Zmsentities\Process as Entity;
6use BO\Zmsentities\Collection\ProcessList as Collection;
7
8/**
9 * @SuppressWarnings(Coupling)
10 */
11class ProcessStatusFree extends Process
12{
13    private function prepareCalendarAndDays(
14        \BO\Zmsentities\Calendar $calendar,
15        \DateTimeInterface $now,
16        $slotsRequired = null
17    ) {
18        $calendar = (new Calendar())->readResolvedEntity($calendar, $now, true);
19        $dayquery = new Day();
20        $dayquery->writeTemporaryScopeList($calendar, $slotsRequired);
21        $selectedDate = $calendar->getFirstDay();
22        $days = [$selectedDate];
23        if ($calendar->getLastDay(false)) {
24            $days = [];
25            while ($selectedDate <= $calendar->getLastDay(false)) {
26                $days[] = $selectedDate;
27                $selectedDate = $selectedDate->modify('+1 day');
28            }
29        }
30        return [$calendar, $dayquery, $days];
31    }
32
33    private function getProcessDataHandle(
34        array $days,
35        $slotType,
36        $slotsRequired,
37        $groupData
38    ) {
39        return $this->fetchHandle(
40            sprintf(
41                Query\ProcessStatusFree::QUERY_SELECT_PROCESSLIST_DAYS,
42                Query\ProcessStatusFree::buildDaysCondition($days)
43            )
44            . ($groupData ? Query\ProcessStatusFree::GROUPBY_SELECT_PROCESSLIST_DAY : ''),
45            [
46                'slotType' => $slotType,
47                'forceRequiredSlots' =>
48                    ($slotsRequired === null || $slotsRequired < 1) ? 1 : intval($slotsRequired),
49            ]
50        );
51    }
52
53    public function readFreeProcesses(
54        \BO\Zmsentities\Calendar $calendar,
55        \DateTimeInterface $now,
56        $slotType = 'public',
57        $slotsRequired = null,
58        $groupData = false
59    ) {
60        list($calendar, $dayquery, $days) = $this->prepareCalendarAndDays($calendar, $now, $slotsRequired);
61        $processData = $this->getProcessDataHandle($days, $slotType, $slotsRequired, $groupData);
62        $processList = new Collection();
63        $scopeList = [];
64        while ($item = $processData->fetch(\PDO::FETCH_ASSOC)) {
65            $process = new \BO\Zmsentities\Process($item);
66            $process->requests = $calendar->requests;
67            $process->appointments->getFirst()->setDateByString(
68                $process->appointments->getFirst()->date,
69                'Y-m-d H:i:s'
70            );
71
72            if (! isset($scopeList[$process->scope->id])) {
73                $scopeList[$process->scope->id] = $calendar->scopes->getEntity($process->scope->id);
74            }
75
76            $process->scope = $scopeList[$process->scope->id];
77            $process->queue['withAppointment'] = 1;
78            $process->appointments->getFirst()->scope = $process->scope;
79            $processList->addEntity($process);
80        }
81        $processData->closeCursor();
82        unset($dayquery);
83        return $processList;
84    }
85
86    public function readFreeProcessesMinimalDeduplicated(
87        \BO\Zmsentities\Calendar $calendar,
88        \DateTimeInterface $now,
89        string $slotType = 'public',
90        ?int $slotsRequired = null,
91        bool $groupData = false
92    ): array {
93        list($calendar, $dayquery, $days) = $this->prepareCalendarAndDays($calendar, $now, $slotsRequired);
94        $processData = $this->getProcessDataHandle($days, $slotType, $slotsRequired, $groupData);
95
96        $unique = [];
97        while ($item = $processData->fetch(\PDO::FETCH_ASSOC)) {
98            $processInfo = $this->extractProcessInfo($item, $calendar);
99            if ($processInfo) {
100                $key = $this->generateUniqueKey($processInfo['providerId'], $processInfo['date']);
101                if (!isset($unique[$key])) {
102                    $unique[$key] = $this->createMinimalProcess($processInfo);
103                }
104            }
105        }
106
107        $processData->closeCursor();
108        unset($dayquery);
109
110        return array_values($unique);
111    }
112
113    private function extractProcessInfo(array $item, \BO\Zmsentities\Calendar $calendar): ?array
114    {
115        $scopeId = $item['scope__id'] ?? null;
116        $dateString = $item['appointments__0__date'] ?? null;
117
118        if (!$scopeId || !$dateString) {
119            return null;
120        }
121
122        $date = strtotime($dateString);
123        if (!$date) {
124            return null;
125        }
126
127        $scope = $calendar->scopes->getEntity($scopeId);
128        if (!$scope) {
129            return null;
130        }
131
132        $providerId = $scope->getProviderId();
133        if (!$providerId) {
134            return null;
135        }
136
137        return [
138            'scopeId' => $scopeId,
139            'providerId' => $providerId,
140            'date' => $date
141        ];
142    }
143
144    private function generateUniqueKey(string $providerId, int $date): string
145    {
146        return $providerId . '_' . $date;
147    }
148
149    private function createMinimalProcess(array $processInfo): array
150    {
151        return [
152            '$schema' => 'https://schema.berlin.de/queuemanagement/process.json',
153            'scope' => [
154                'id' => $processInfo['scopeId'],
155                'provider' => [
156                    'id' => $processInfo['providerId']
157                ]
158            ],
159            'appointments' => [
160                [
161                    'date' => (string)$processInfo['date'],
162                    'scope' => [
163                        'id' => $processInfo['scopeId'],
164                        'provider' => [
165                            'id' => $processInfo['providerId']
166                        ]
167                    ]
168                ]
169            ]
170        ];
171    }
172
173    public function readReservedProcesses($resolveReferences = 2)
174    {
175        $processList = new Collection();
176        $query = new Query\Process(Query\Base::SELECT);
177        $query
178            ->addResolvedReferences($resolveReferences)
179            ->addEntityMapping()
180            ->addConditionAssigned()
181            ->addConditionIsReserved();
182        $resultData = $this->fetchList($query, new Entity());
183        foreach ($resultData as $process) {
184            if (2 == $resolveReferences) {
185                $process['requests'] = (new Request())->readRequestByProcessId($process->id, $resolveReferences);
186                $process['scope'] = (new Scope())->readEntity($process->getScopeId(), $resolveReferences);
187            }
188            if ($process instanceof Entity) {
189                $processList->addEntity($process);
190            }
191        }
192        return $processList;
193    }
194
195    /**
196     * Insert a new process if there are free slots
197     *
198     * @param \BO\Zmsentities\Process $process
199     * @param \DateTimeInterface $now
200     * @param String $slotType
201     * @param Int $slotsRequired we cannot use process.appointments.0.slotCount, because setting slotsRequired is
202     *        a priviliged operation. Just using the input would be a security flaw to get a wider selection of times
203     *        If slotsRequired = 0, readFreeProcesses() uses the slotsRequired based on request-provider relation
204     */
205    public function writeEntityReserved(
206        \BO\Zmsentities\Process $process,
207        \DateTimeInterface $now,
208        $slotType = "public",
209        $slotsRequired = 0,
210        $resolveReferences = 0,
211        $userAccount = null
212    ) {
213        $process = clone $process;
214        $process->status = 'reserved';
215        $appointment = $process->getAppointments()->getFirst();
216        $slotList = (new Slot())->readByAppointment(
217            $appointment,
218            $slotsRequired,
219            (null !== $userAccount),
220            true
221        );
222        $freeProcessList = $this->readFreeProcesses($process->toCalendar(), $now, $slotType, $slotsRequired);
223
224        if (!$freeProcessList->getAppointmentList()->hasAppointment($appointment) || ! $slotList) {
225            throw new Exception\Process\ProcessReserveFailed();
226        }
227
228        foreach ($slotList as $slot) {
229            if ($process->id > 99999) {
230                $newProcess = clone $process;
231                $newProcess->getFirstAppointment()->setTime($slot->time);
232                $this->writeNewProcess($newProcess, $now, $process->id, 0, true, $userAccount);
233            } elseif ($process->id === 0) {
234                $process = $this->writeNewProcess($process, $now, 0, count($slotList) - 1, true, $userAccount);
235            } else {
236                throw new \Exception("SQL UPDATE error on inserting new $process on $slot");
237            }
238        }
239        $this->writeRequestsToDb($process);
240        return $this->readEntity($process->getId(), new Helper\NoAuth(), $resolveReferences);
241    }
242}