Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.97% covered (warning)
76.97%
234 / 304
43.48% covered (danger)
43.48%
10 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Slot
76.97% covered (warning)
76.97%
234 / 304
43.48% covered (danger)
43.48%
10 / 23
192.10
0.00% covered (danger)
0.00%
0 / 1
 readByAppointment
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 readByAvailability
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 hasScopeRelevantChanges
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
5.06
 isAvailabilityOutdated
95.56% covered (success)
95.56%
43 / 45
0.00% covered (danger)
0.00%
0 / 1
18
 writeByAvailability
77.78% covered (warning)
77.78%
42 / 54
0.00% covered (danger)
0.00%
0 / 1
20.17
 writeByScope
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 writeSlotListForDate
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
7.03
 writeAncestorIDs
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 readLastChangedTime
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 readLastChangedTimeByScope
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 readLastChangedTimeByAvailability
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateSlotProcessMapping
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 deleteSlotProcessOnSlot
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 deleteSlotProcessOnProcess
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 writeSlotProcessMappingFor
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 deleteSlotProcessMappingFor
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 writeCanceledByTimeAndScope
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 writeCanceledByTime
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 deleteSlotsOlderThan
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 readRowsByScopeAndDate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 writeOptimizedSlotTables
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
4.20
 getLastGeneratedSlotDate
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 getOldestSlotVersionByAvailability
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace BO\Zmsdb;
4
5use BO\Zmsdldb\Helper\DateTime;
6use BO\Zmsentities\Slot as Entity;
7use BO\Zmsentities\Collection\SlotList as Collection;
8use BO\Zmsentities\Availability as AvailabilityEntity;
9use BO\Zmsentities\Scope as ScopeEntity;
10
11/**
12 * @SuppressWarnings(Public)
13 * @SuppressWarnings(Complexity)
14 * @SuppressWarnings(Coupling)
15 */
16class Slot extends Base
17{
18    /**
19     * maximum number of slots per appointment
20     */
21    const MAX_SLOTS = 25;
22
23    const MAX_DAYS_OF_SLOT_CALCULATION = 180;
24
25    /**
26     * @return \BO\Zmsentities\Collection\SlotList
27     *
28     */
29    public function readByAppointment(
30        \BO\Zmsentities\Appointment $appointment,
31        $overwriteSlotsCount = null,
32        $extendSlotList = false,
33        $lockSlots = false
34    ) {
35        $appointment = clone $appointment;
36        $availability = (new Availability())->readByAppointment($appointment);
37        // Check if availability allows multiple slots, but allow overwrite
38        if (!$availability->multipleSlotsAllowed || $overwriteSlotsCount >= 1) {
39            $appointment->slotCount = ($overwriteSlotsCount >= 1) ? $overwriteSlotsCount : 1;
40        }
41        $slotList = $availability->getSlotList()->withSlotsForAppointment($appointment, $extendSlotList);
42        foreach ($slotList as $slot) {
43            $this->readByAvailability($slot, $availability, $appointment->toDateTime(), $lockSlots);
44        }
45        return $slotList;
46    }
47
48    public function readByAvailability(
49        \BO\Zmsentities\Slot $slot,
50        AvailabilityEntity $availability,
51        \DateTimeInterface $date,
52        $getLock = false
53    ) {
54        $data = array();
55        $data['scopeID'] = $availability->scope->id;
56        $data['availabilityID'] = $availability->id;
57        $data['year'] = $date->format('Y');
58        $data['month'] = $date->format('m');
59        $data['day'] = $date->format('d');
60        $data['time'] = $slot->getTimeString();
61        $sql = Query\Slot::QUERY_SELECT_SLOT;
62        if ($getLock) {
63            $sql .= ' FOR UPDATE';
64        }
65        $slotID = $this->fetchRow(
66            $sql,
67            $data
68        );
69        return $slotID ? $slotID['slotID'] : false ;
70    }
71
72    public function hasScopeRelevantChanges(
73        \BO\Zmsentities\Scope $scope,
74        \DateTimeInterface $slotLastChange = null
75    ) {
76        $startInDaysDefault = (new Preferences())
77            ->readProperty('scope', $scope->id, 'appointment', 'startInDaysDefault');
78        $endInDaysDefault = (new Preferences())
79            ->readProperty('scope', $scope->id, 'appointment', 'endInDaysDefault');
80        if (
81            $scope->preferences['appointment']['startInDaysDefault'] != $startInDaysDefault
82            || $scope->preferences['appointment']['endInDaysDefault'] != $endInDaysDefault
83        ) {
84            (new Scope())->replacePreferences($scope); //TODO remove after ZMS1 is deactivated
85            return true;
86        }
87        $startInDaysChange = (new Preferences())
88            ->readChangeDateTime('scope', $scope->id, 'appointment', 'startInDaysDefault');
89        $endInDaysChange = (new Preferences())
90            ->readChangeDateTime('scope', $scope->id, 'appointment', 'endInDaysDefault');
91        if (
92            $startInDaysChange->getTimestamp() > $slotLastChange->getTimestamp()
93            || $endInDaysChange->getTimestamp() > $slotLastChange->getTimestamp()
94        ) {
95            return true;
96        }
97    }
98
99    public function isAvailabilityOutdated(
100        \BO\Zmsentities\Availability $availability,
101        \DateTimeInterface $now,
102        \DateTimeInterface $slotLastChange = null,
103        int $oldestSlotVersion = 1
104    ) {
105        $proposedChange = new Helper\AvailabilitySnapShot($availability, $now);
106        $formerChange = new Helper\AvailabilitySnapShot($availability, $slotLastChange);
107
108        if ($availability->version > $oldestSlotVersion) {
109            $availability['processingNote'][] = 'outdated: availability version change';
110            return true;
111        }
112
113        if ($formerChange->hasOutdatedAvailability()) {
114            $availability['processingNote'][] = 'outdated: availability change';
115            return true;
116        }
117        if (
118            $formerChange->hasOutdatedScope()
119            && $this->hasScopeRelevantChanges($availability->scope, $slotLastChange)
120        ) {
121            $availability['processingNote'][] = 'outdated: scope change';
122            return true;
123        }
124        if ($formerChange->hasOutdatedDayoff()) {
125            $availability['processingNote'][] = 'outdated: dayoff change';
126            return true;
127        }
128        // Be aware, that last slot change and current time might differ serval days
129        //  if the rebuild fails in some way
130        if (
131            1
132            // First check if the bookable end date on current time was already calculated on last slot change
133            && !$formerChange->hasBookableDateTime($proposedChange->getLastBookableDateTime())
134            // Second check if between last slot change and current time could be a bookable slot
135            && (
136                (
137                    !$formerChange->isOpenedOnLastBookableDay()
138                    && $proposedChange->hasBookableDateTimeAfter($formerChange->getLastBookableDateTime())
139                )
140                // if calculation already happened the day before, check if lastChange time was before opening
141                || (
142                    $formerChange->isOpenedOnLastBookableDay()
143                    && (
144                        !$formerChange->isTimeOpenedOnLastBookableDay()
145                        || $proposedChange->hasBookableDateTimeAfter(
146                            $formerChange->getLastBookableDateTime()->modify('+1day 00:00:00')
147                        )
148                    )
149                )
150            )
151            // Check if daytime is after booking start time if bookable end of now is calculated
152            && (
153                !$proposedChange->isOpenedOnLastBookableDay()
154                || $proposedChange->isTimeOpenedOnLastBookableDay()
155            )
156        ) {
157            $availability['processingNote'][] = 'outdated: new slots required';
158            return true;
159        }
160        if (
161            $availability->getBookableStart($slotLastChange) != $availability->getBookableStart($now)
162            // First check, if bookable start from lastChange was not included in bookable time from now
163            && !$availability->hasDate($availability->getBookableStart($slotLastChange), $now)
164            // Second check, if availability had a bookable time on lastChange before bookable start from now
165            && $availability->hasDateBetween(
166                $availability->getBookableStart($slotLastChange),
167                $availability->getBookableStart($now),
168                $slotLastChange
169            )
170        ) {
171            $availability['processingNote'][] = 'outdated: slots invalidated by bookable start';
172            return true;
173        }
174        $availability['processingNote'][] = 'not outdated';
175        return false;
176    }
177
178    /**
179     * @return bool TRUE if there were changes on slots
180     */
181    public function writeByAvailability(
182        \BO\Zmsentities\Availability $availability,
183        \DateTimeInterface $now,
184        \DateTimeInterface $slotLastChange = null
185    ) {
186        $now = \BO\Zmsentities\Helper\DateTime::create($now);
187        $calculateSlotsUntilDate = \BO\Zmsentities\Helper\DateTime::create($now)->modify('+' . self::MAX_DAYS_OF_SLOT_CALCULATION . ' days');
188        if (!$slotLastChange) {
189            $slotLastChange = $this->readLastChangedTimeByAvailability($availability);
190        }
191        $lastGeneratedSlotDate = $this->getLastGeneratedSlotDate($availability);
192        $oldestSlotVersion = $this->getOldestSlotVersionByAvailability($availability);
193        $availability['processingNote'][] = 'lastchange=' . $slotLastChange->format('c');
194        if (!$this->isAvailabilityOutdated($availability, $now, $slotLastChange, $oldestSlotVersion)) {
195            return false;
196        }
197        $startDate = $availability->getBookableStart($now)->modify('00:00:00');
198        $stopDate = $availability->getBookableEnd($now);
199        $generateNew = empty($lastGeneratedSlotDate) || $availability->version > $oldestSlotVersion;
200        (new Availability())->readLock($availability->id);
201        $cancelledSlots = 0;
202        $cancelledSlots += $this->fetchAffected(Query\Slot::QUERY_CANCEL_AVAILABILITY_BEFORE_BOOKABLE, [
203            'availabilityID' => $availability->id,
204            'providedDate' => $startDate->format('Y-m-d')
205        ]);
206        // Cancel slots only if previously generated beyond new bookable end
207        if ($lastGeneratedSlotDate && $lastGeneratedSlotDate->getTimestamp() > $stopDate->getTimestamp()) {
208            $cancelledSlots += $this->fetchAffected(Query\Slot::QUERY_CANCEL_AVAILABILITY_AFTER_BOOKABLE, [
209                'availabilityID' => $availability->id,
210                'providedDate' => $stopDate->format('Y-m-d')
211            ]);
212        }
213        if ($generateNew) {
214            $cancelledSlots += $this->fetchAffected(Query\Slot::QUERY_CANCEL_AVAILABILITY, [
215                'availabilityID' => $availability->id,
216            ]);
217
218            if (!$availability->withData(['bookable' => ['startInDays' => 0]])->hasBookableDates($now)) {
219                $availability['processingNote'][] = "cancelled $cancelledSlots slots: availability not bookable ";
220                return ($cancelledSlots > 0) ? true : false;
221            }
222
223            \App::$log->info('availability: ', [
224                'generate_new' => $generateNew,
225                'availability_id' => $availability->id,
226                'cancelledSlots' => $cancelledSlots
227            ]);
228
229            $availability['processingNote'][] = "cancelled $cancelledSlots slots";
230        } elseif ($cancelledSlots > 0) {
231            $availability['processingNote'][] = "cancelled $cancelledSlots slots";
232        }
233
234        $slotlist = $availability->getSlotList();
235        $slotlistIntern = $slotlist->withValueFor('callcenter', 0)->withValueFor('public', 0);
236        $time = $now->modify('00:00:00');
237        if (!$generateNew) {
238            $time = $lastGeneratedSlotDate->modify('+1 day')->modify('00:00:00');
239        }
240        $status = false;
241        do {
242            if ($availability->withData(['bookable' => ['startInDays' => 0]])->hasDate($time, $now)) {
243                $writeStatus = $this->writeSlotListForDate(
244                    $time,
245                    ($time->getTimestamp() < $startDate->getTimestamp()) ? $slotlistIntern : $slotlist,
246                    $availability
247                );
248                $status = $writeStatus ? $writeStatus : $status;
249            }
250            $time = $time->modify('+1day');
251        } while ($time->getTimestamp() <= $stopDate->getTimestamp() && $time->getTimestamp() < $calculateSlotsUntilDate->getTimestamp());
252
253        return $status || (isset($cancelledSlots) && $cancelledSlots > 0);
254    }
255
256    public function writeByScope(\BO\Zmsentities\Scope $scope, \DateTimeInterface $now)
257    {
258        $slotLastChange = $this->readLastChangedTimeByScope($scope);
259        $availabilityList = (new \BO\Zmsdb\Availability())
260            ->readAvailabilityListByScope($scope, 0, $slotLastChange->modify('-1 day'))
261            ;
262        $updatedList = new \BO\Zmsentities\Collection\AvailabilityList();
263        foreach ($availabilityList as $availability) {
264            $availability->scope = clone $scope; //dayoff is required
265            if ($this->writeByAvailability($availability, $now)) {
266                $updatedList->addEntity($availability);
267            }
268        }
269        return $updatedList;
270    }
271
272    protected function writeSlotListForDate(
273        \DateTimeInterface $time,
274        Collection $slotlist,
275        AvailabilityEntity $availability
276    ) {
277        $ancestors = [];
278        $hasAddedSlots = false;
279
280        foreach ($slotlist as $slot) {
281            $slot = clone $slot;
282            $slotID = $this->readByAvailability($slot, $availability, $time);
283            if ($slotID) {
284                $query = new Query\Slot(Query\Base::UPDATE);
285                $query->addConditionSlotId($slotID);
286            } else {
287                $query = new Query\Slot(Query\Base::INSERT);
288                $hasAddedSlots = true;
289            }
290            $slot->status = 'free';
291            $values = $query->reverseEntityMapping($slot, $availability, $time);
292            $values['createTimestamp'] = time();
293            $query->addValues($values);
294            $writeStatus = $this->writeItem($query);
295            if ($writeStatus && !$slotID) {
296                $slotID = $this->getWriter()->lastInsertId();
297            }
298            $ancestors[] = $slotID;
299            // TODO: Check if slot changed before writing ancestor IDs
300            $this->writeAncestorIDs($slotID, $ancestors);
301            $status = $writeStatus ? $writeStatus : $status;
302        }
303        if ($hasAddedSlots) {
304            $availability['processingNote'][] = 'Added ' . $time->format('Y-m-d');
305        }
306        return $status;
307    }
308
309    protected function writeAncestorIDs($slotID, array $ancestors)
310    {
311        $this->perform(Query\Slot::QUERY_DELETE_ANCESTOR, [
312            'slotID' => $slotID,
313        ]);
314        $ancestorLevel = count($ancestors);
315        foreach ($ancestors as $ancestorID) {
316            if ($ancestorLevel <= self::MAX_SLOTS) {
317                $this->perform(Query\Slot::QUERY_INSERT_ANCESTOR, [
318                    'slotID' => $slotID,
319                    'ancestorID' => $ancestorID,
320                    'ancestorLevel' => $ancestorLevel,
321                ]);
322            }
323            $ancestorLevel--;
324        }
325    }
326
327    public function readLastChangedTime()
328    {
329        $last = $this->fetchRow(
330            Query\Slot::QUERY_LAST_CHANGED
331        );
332        if (!$last['dateString']) {
333            $last['dateString'] = '1970-01-01 12:00';
334        }
335        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
336    }
337
338    public function readLastChangedTimeByScope(ScopeEntity $scope)
339    {
340        $last = $this->fetchRow(
341            Query\Slot::QUERY_LAST_CHANGED_SCOPE,
342            [
343                'scopeID' => $scope->id,
344            ]
345        );
346        if (!$last['dateString']) {
347            $last['dateString'] = '1970-01-01 12:00';
348        }
349        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
350    }
351
352    public function readLastChangedTimeByAvailability(AvailabilityEntity $availabiliy)
353    {
354        $last = $this->fetchRow(
355            Query\Slot::QUERY_LAST_CHANGED_AVAILABILITY,
356            [
357                'availabilityID' => $availabiliy->id,
358            ]
359        );
360        if (!$last['dateString']) {
361            $last['dateString'] = '1970-01-01 12:00';
362        }
363        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
364    }
365
366    public function updateSlotProcessMapping($scopeID = null)
367    {
368        if ($scopeID) {
369            $processIdList = $this->fetchAll(
370                Query\Slot::QUERY_SELECT_MISSING_PROCESS
371                . Query\Slot::QUERY_SELECT_MISSING_PROCESS_BY_SCOPE,
372                ['scopeID' => $scopeID]
373            );
374        } else {
375            $processIdList = $this->fetchAll(Query\Slot::QUERY_SELECT_MISSING_PROCESS, []);
376        }
377        // Client side INSERT ... SELECT ... to reduce table locking
378        foreach ($processIdList as $processId) {
379            $this->perform(Query\Slot::QUERY_INSERT_SLOT_PROCESS, array_values($processId));
380        }
381        return count($processIdList);
382    }
383
384    public function deleteSlotProcessOnSlot($scopeID = null)
385    {
386        if ($scopeID) {
387            $this->perform(
388                Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED
389                . Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED_BY_SCOPE,
390                ['scopeID' => $scopeID]
391            );
392        } else {
393            $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED, []);
394        }
395    }
396
397    public function deleteSlotProcessOnProcess($scopeID = null)
398    {
399        if ($scopeID) {
400            $processIdList = $this->fetchAll(
401                Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS
402                . Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS_BY_SCOPE,
403                ['scopeID' => $scopeID]
404            );
405        } else {
406            $processIdList = $this->fetchAll(Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS);
407        }
408        // Client side INSERT ... SELECT ... to reduce table locking
409        foreach ($processIdList as $processId) {
410            $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_ID, $processId);
411        }
412        return count($processIdList);
413    }
414
415    public function writeSlotProcessMappingFor($processId)
416    {
417        $this->perform(Query\Slot::QUERY_INSERT_SLOT_PROCESS_ID, [
418            'processId' => $processId,
419        ]);
420        return $this;
421    }
422
423    public function deleteSlotProcessMappingFor($processId)
424    {
425        $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_ID, [
426            'processId' => $processId,
427        ]);
428        return $this;
429    }
430
431    public function writeCanceledByTimeAndScope(\DateTimeInterface $dateTime, \BO\Zmsentities\Scope $scope)
432    {
433        $status = $this->perform(Query\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY_BY_SCOPE, [
434            'dateString' => $dateTime->format('Y-m-d'),
435            'scopeID' => $scope->id,
436        ]);
437
438        return $this->perform(Query\Slot::QUERY_CANCEL_SLOT_OLD_BY_SCOPE, [
439            'year' => $dateTime->format('Y'),
440            'month' => $dateTime->format('m'),
441            'day' => $dateTime->format('d'),
442            'time' => $dateTime->format('H:i:s'),
443            'scopeID' => $scope->id,
444        ]) && $status;
445    }
446
447    public function writeCanceledByTime(\DateTimeInterface $dateTime)
448    {
449        $status = $this->perform(Query\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY, [
450            'dateString' => $dateTime->format('Y-m-d'),
451        ]);
452        return $this->perform(Query\Slot::QUERY_CANCEL_SLOT_OLD, [
453            'year' => $dateTime->format('Y'),
454            'month' => $dateTime->format('m'),
455            'day' => $dateTime->format('d'),
456            'time' => $dateTime->format('H:i:s'),
457        ]) && $status;
458    }
459
460    public function deleteSlotsOlderThan(\DateTimeInterface $dateTime)
461    {
462        $status = $this->perform(Query\Slot::QUERY_DELETE_SLOT_OLD, [
463            'year' => $dateTime->format('Y'),
464            'month' => $dateTime->format('m'),
465            'day' => $dateTime->format('d'),
466        ]);
467        $status = ($status && $this->perform(Query\Slot::QUERY_DELETE_SLOT_HIERA));
468        return $status;
469    }
470
471    /**
472     * This function is for debugging
473     */
474    public function readRowsByScopeAndDate(
475        \BO\Zmsentities\Scope $scope,
476        \DateTimeInterface $dateTime
477    ) {
478        $list = $this->fetchAll(Query\Slot::QUERY_SELECT_BY_SCOPE_AND_DAY, [
479            'year' => $dateTime->format('Y'),
480            'month' => $dateTime->format('m'),
481            'day' => $dateTime->format('d'),
482            'scopeID' => $scope->id,
483        ]);
484        return $list;
485    }
486
487    public function writeOptimizedSlotTables()
488    {
489        $queries = [
490            Query\Slot::QUERY_OPTIMIZE_SLOT,
491            Query\Slot::QUERY_OPTIMIZE_SLOT_HIERA,
492            Query\Slot::QUERY_OPTIMIZE_SLOT_PROCESS,
493            Query\Slot::QUERY_OPTIMIZE_PROCESS,
494        ];
495
496        $status = true;
497        foreach ($queries as $query) {
498            try {
499                $status = $status && $this->perform($query);
500            } catch (\PDOException $e) {
501                \App::$log->error("Failed to optimize table with query: $query. Error: " . $e->getMessage(), []);
502
503                return false;
504            }
505        }
506
507        return $status;
508    }
509
510    private function getLastGeneratedSlotDate(AvailabilityEntity $availability)
511    {
512        $last = $this->fetchRow(
513            Query\Slot::QUERY_LAST_IN_AVAILABILITY,
514            [
515                'availabilityID' => $availability->id,
516            ]
517        );
518
519        if (!isset($last['dateString'])) {
520            return null;
521        }
522
523        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
524    }
525
526    private function getOldestSlotVersionByAvailability(AvailabilityEntity $availability)
527    {
528        $last = $this->fetchRow(
529            Query\Slot::QUERY_OLDEST_VERSION_IN_AVAILABILITY,
530            [
531                'availabilityID' => $availability->id,
532            ]
533        );
534
535        return $last['version'] ?? 1;
536    }
537}