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