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