Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.47% covered (warning)
78.47%
215 / 274
54.55% covered (warning)
54.55%
12 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Slot
78.47% covered (warning)
78.47%
215 / 274
54.55% covered (warning)
54.55%
12 / 22
159.84
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
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
17
 writeByAvailability
82.50% covered (warning)
82.50%
33 / 40
0.00% covered (danger)
0.00%
0 / 1
13.91
 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
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getLastGeneratedSlotDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace BO\Zmsdb;
4
5use BO\Dldb\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    ) {
104        $proposedChange = new Helper\AvailabilitySnapShot($availability, $now);
105        $formerChange = new Helper\AvailabilitySnapShot($availability, $slotLastChange);
106
107        if ($formerChange->hasOutdatedAvailability()) {
108            $availability['processingNote'][] = 'outdated: availability change';
109            return true;
110        }
111        if (
112            $formerChange->hasOutdatedScope()
113            && $this->hasScopeRelevantChanges($availability->scope, $slotLastChange)
114        ) {
115            $availability['processingNote'][] = 'outdated: scope change';
116            return true;
117        }
118        if ($formerChange->hasOutdatedDayoff()) {
119            $availability['processingNote'][] = 'outdated: dayoff change';
120            return true;
121        }
122        // Be aware, that last slot change and current time might differ serval days
123        //  if the rebuild fails in some way
124        if (
125            1
126            // First check if the bookable end date on current time was already calculated on last slot change
127            && !$formerChange->hasBookableDateTime($proposedChange->getLastBookableDateTime())
128            // Second check if between last slot change and current time could be a bookable slot
129            && (
130                (
131                    !$formerChange->isOpenedOnLastBookableDay()
132                    && $proposedChange->hasBookableDateTimeAfter($formerChange->getLastBookableDateTime())
133                )
134                // if calculation already happened the day before, check if lastChange time was before opening
135                || (
136                    $formerChange->isOpenedOnLastBookableDay()
137                    && (
138                        !$formerChange->isTimeOpenedOnLastBookableDay()
139                        || $proposedChange->hasBookableDateTimeAfter(
140                            $formerChange->getLastBookableDateTime()->modify('+1day 00:00:00')
141                        )
142                    )
143                )
144            )
145            // Check if daytime is after booking start time if bookable end of now is calculated
146            && (
147                !$proposedChange->isOpenedOnLastBookableDay()
148                || $proposedChange->isTimeOpenedOnLastBookableDay()
149            )
150        ) {
151            $availability['processingNote'][] = 'outdated: new slots required';
152            return true;
153        }
154        if (
155            $availability->getBookableStart($slotLastChange) != $availability->getBookableStart($now)
156            // First check, if bookable start from lastChange was not included in bookable time from now
157            && !$availability->hasDate($availability->getBookableStart($slotLastChange), $now)
158            // Second check, if availability had a bookable time on lastChange before bookable start from now
159            && $availability->hasDateBetween(
160                $availability->getBookableStart($slotLastChange),
161                $availability->getBookableStart($now),
162                $slotLastChange
163            )
164        ) {
165            $availability['processingNote'][] = 'outdated: slots invalidated by bookable start';
166            return true;
167        }
168        $availability['processingNote'][] = 'not outdated';
169        return false;
170    }
171
172    /**
173     * @return bool TRUE if there were changes on slots
174     */
175    public function writeByAvailability(
176        \BO\Zmsentities\Availability $availability,
177        \DateTimeInterface $now,
178        \DateTimeInterface $slotLastChange = null
179    ) {
180        $now = \BO\Zmsentities\Helper\DateTime::create($now);
181        $calculateSlotsUntilDate = \BO\Zmsentities\Helper\DateTime::create($now)->modify('+' . self::MAX_DAYS_OF_SLOT_CALCULATION . ' days');
182        if (!$slotLastChange) {
183            $slotLastChange = $this->readLastChangedTimeByAvailability($availability);
184        }
185        $lastGeneratedSlotDate = $this->getLastGeneratedSlotDate($availability);
186
187        $availability['processingNote'][] = 'lastchange=' . $slotLastChange->format('c');
188        if (!$this->isAvailabilityOutdated($availability, $now, $slotLastChange)) {
189            return false;
190        }
191        $startDate = $availability->getBookableStart($now)->modify('00:00:00');
192        $stopDate = $availability->getBookableEnd($now);
193        $generateNew = $availability->isNewerThan($slotLastChange);
194        (new Availability())->readLock($availability->id);
195        $cancelledSlots = $this->fetchAffected(Query\Slot::QUERY_CANCEL_AVAILABILITY_BEFORE_BOOKABLE, [
196            'availabilityID' => $availability->id,
197            'providedDate' => $startDate->format('Y-m-d')
198        ]);
199        if ($generateNew) {
200            $cancelledSlots = $this->fetchAffected(Query\Slot::QUERY_CANCEL_AVAILABILITY, [
201                'availabilityID' => $availability->id,
202            ]);
203            if (!$availability->withData(['bookable' => ['startInDays' => 0]])->hasBookableDates($now)) {
204                $availability['processingNote'][] = "cancelled $cancelledSlots slots: availability not bookable ";
205                return ($cancelledSlots > 0) ? true : false;
206            }
207            $availability['processingNote'][] = "cancelled $cancelledSlots slots";
208        }
209
210        $slotlist = $availability->getSlotList();
211        $slotlistIntern = $slotlist->withValueFor('callcenter', 0)->withValueFor('public', 0);
212        $time = $now->modify('00:00:00');
213        if (!$generateNew) {
214            $time = $lastGeneratedSlotDate->modify('+1 day')->modify('00:00:00');
215        }
216        $status = false;
217        do {
218            if ($availability->withData(['bookable' => ['startInDays' => 0]])->hasDate($time, $now)) {
219                $writeStatus = $this->writeSlotListForDate(
220                    $time,
221                    ($time->getTimestamp() < $startDate->getTimestamp()) ? $slotlistIntern : $slotlist,
222                    $availability
223                );
224                $status = $writeStatus ? $writeStatus : $status;
225            }
226            $time = $time->modify('+1day');
227        } while ($time->getTimestamp() <= $stopDate->getTimestamp() && $time->getTimestamp() < $calculateSlotsUntilDate->getTimestamp());
228
229        return $status || (isset($cancelledSlots) && $cancelledSlots > 0);
230    }
231
232    public function writeByScope(\BO\Zmsentities\Scope $scope, \DateTimeInterface $now)
233    {
234        $slotLastChange = $this->readLastChangedTimeByScope($scope);
235        $availabilityList = (new \BO\Zmsdb\Availability())
236            ->readAvailabilityListByScope($scope, 0, $slotLastChange->modify('-1 day'))
237            ;
238        $updatedList = new \BO\Zmsentities\Collection\AvailabilityList();
239        foreach ($availabilityList as $availability) {
240            $availability->scope = clone $scope; //dayoff is required
241            if ($this->writeByAvailability($availability, $now)) {
242                $updatedList->addEntity($availability);
243            }
244        }
245        return $updatedList;
246    }
247
248    protected function writeSlotListForDate(
249        \DateTimeInterface $time,
250        Collection $slotlist,
251        AvailabilityEntity $availability
252    ) {
253        $ancestors = [];
254        $hasAddedSlots = false;
255        foreach ($slotlist as $slot) {
256            $slot = clone $slot;
257            $slotID = $this->readByAvailability($slot, $availability, $time);
258            if ($slotID) {
259                $query = new Query\Slot(Query\Base::UPDATE);
260                $query->addConditionSlotId($slotID);
261            } else {
262                $query = new Query\Slot(Query\Base::INSERT);
263                $hasAddedSlots = true;
264            }
265            $slot->status = 'free';
266            $values = $query->reverseEntityMapping($slot, $availability, $time);
267            $values['createTimestamp'] = time();
268            $query->addValues($values);
269            $writeStatus = $this->writeItem($query);
270            if ($writeStatus && !$slotID) {
271                $slotID = $this->getWriter()->lastInsertId();
272            }
273            $ancestors[] = $slotID;
274            // TODO: Check if slot changed before writing ancestor IDs
275            $this->writeAncestorIDs($slotID, $ancestors);
276            $status = $writeStatus ? $writeStatus : $status;
277        }
278        if ($hasAddedSlots) {
279            $availability['processingNote'][] = 'Added ' . $time->format('Y-m-d');
280        }
281        return $status;
282    }
283
284    protected function writeAncestorIDs($slotID, array $ancestors)
285    {
286        $this->perform(Query\Slot::QUERY_DELETE_ANCESTOR, [
287            'slotID' => $slotID,
288        ]);
289        $ancestorLevel = count($ancestors);
290        foreach ($ancestors as $ancestorID) {
291            if ($ancestorLevel <= self::MAX_SLOTS) {
292                $this->perform(Query\Slot::QUERY_INSERT_ANCESTOR, [
293                    'slotID' => $slotID,
294                    'ancestorID' => $ancestorID,
295                    'ancestorLevel' => $ancestorLevel,
296                ]);
297            }
298            $ancestorLevel--;
299        }
300    }
301
302    public function readLastChangedTime()
303    {
304        $last = $this->fetchRow(
305            Query\Slot::QUERY_LAST_CHANGED
306        );
307        if (!$last['dateString']) {
308            $last['dateString'] = '1970-01-01 12:00';
309        }
310        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
311    }
312
313    public function readLastChangedTimeByScope(ScopeEntity $scope)
314    {
315        $last = $this->fetchRow(
316            Query\Slot::QUERY_LAST_CHANGED_SCOPE,
317            [
318                'scopeID' => $scope->id,
319            ]
320        );
321        if (!$last['dateString']) {
322            $last['dateString'] = '1970-01-01 12:00';
323        }
324        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
325    }
326
327    public function readLastChangedTimeByAvailability(AvailabilityEntity $availabiliy)
328    {
329        $last = $this->fetchRow(
330            Query\Slot::QUERY_LAST_CHANGED_AVAILABILITY,
331            [
332                'availabilityID' => $availabiliy->id,
333            ]
334        );
335        if (!$last['dateString']) {
336            $last['dateString'] = '1970-01-01 12:00';
337        }
338        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsdb\Connection\Select::$connectionTimezone);
339    }
340
341    public function updateSlotProcessMapping($scopeID = null)
342    {
343        if ($scopeID) {
344            $processIdList = $this->fetchAll(
345                Query\Slot::QUERY_SELECT_MISSING_PROCESS
346                . Query\Slot::QUERY_SELECT_MISSING_PROCESS_BY_SCOPE,
347                ['scopeID' => $scopeID]
348            );
349        } else {
350            $processIdList = $this->fetchAll(Query\Slot::QUERY_SELECT_MISSING_PROCESS, []);
351        }
352        // Client side INSERT ... SELECT ... to reduce table locking
353        foreach ($processIdList as $processId) {
354            $this->perform(Query\Slot::QUERY_INSERT_SLOT_PROCESS, array_values($processId));
355        }
356        return count($processIdList);
357    }
358
359    public function deleteSlotProcessOnSlot($scopeID = null)
360    {
361        if ($scopeID) {
362            $this->perform(
363                Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED
364                . Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED_BY_SCOPE,
365                ['scopeID' => $scopeID]
366            );
367        } else {
368            $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED, []);
369        }
370    }
371
372    public function deleteSlotProcessOnProcess($scopeID = null)
373    {
374        if ($scopeID) {
375            $processIdList = $this->fetchAll(
376                Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS
377                . Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS_BY_SCOPE,
378                ['scopeID' => $scopeID]
379            );
380        } else {
381            $processIdList = $this->fetchAll(Query\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS);
382        }
383        // Client side INSERT ... SELECT ... to reduce table locking
384        foreach ($processIdList as $processId) {
385            $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_ID, $processId);
386        }
387        return count($processIdList);
388    }
389
390    public function writeSlotProcessMappingFor($processId)
391    {
392        $this->perform(Query\Slot::QUERY_INSERT_SLOT_PROCESS_ID, [
393            'processId' => $processId,
394        ]);
395        return $this;
396    }
397
398    public function deleteSlotProcessMappingFor($processId)
399    {
400        $this->perform(Query\Slot::QUERY_DELETE_SLOT_PROCESS_ID, [
401            'processId' => $processId,
402        ]);
403        return $this;
404    }
405
406    public function writeCanceledByTimeAndScope(\DateTimeInterface $dateTime, \BO\Zmsentities\Scope $scope)
407    {
408        $status = $this->perform(Query\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY_BY_SCOPE, [
409            'dateString' => $dateTime->format('Y-m-d'),
410            'scopeID' => $scope->id,
411        ]);
412        return $this->perform(Query\Slot::QUERY_CANCEL_SLOT_OLD_BY_SCOPE, [
413            'year' => $dateTime->format('Y'),
414            'month' => $dateTime->format('m'),
415            'day' => $dateTime->format('d'),
416            'time' => $dateTime->format('H:i:s'),
417            'scopeID' => $scope->id,
418        ]) && $status;
419    }
420
421    public function writeCanceledByTime(\DateTimeInterface $dateTime)
422    {
423        $status = $this->perform(Query\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY, [
424            'dateString' => $dateTime->format('Y-m-d'),
425        ]);
426        return $this->perform(Query\Slot::QUERY_CANCEL_SLOT_OLD, [
427            'year' => $dateTime->format('Y'),
428            'month' => $dateTime->format('m'),
429            'day' => $dateTime->format('d'),
430            'time' => $dateTime->format('H:i:s'),
431        ]) && $status;
432    }
433
434    public function deleteSlotsOlderThan(\DateTimeInterface $dateTime)
435    {
436        $status = $this->perform(Query\Slot::QUERY_DELETE_SLOT_OLD, [
437            'year' => $dateTime->format('Y'),
438            'month' => $dateTime->format('m'),
439            'day' => $dateTime->format('d'),
440        ]);
441        $status = ($status && $this->perform(Query\Slot::QUERY_DELETE_SLOT_HIERA));
442        return $status;
443    }
444
445    /**
446     * This function is for debugging
447     */
448    public function readRowsByScopeAndDate(
449        \BO\Zmsentities\Scope $scope,
450        \DateTimeInterface $dateTime
451    ) {
452        $list = $this->fetchAll(Query\Slot::QUERY_SELECT_BY_SCOPE_AND_DAY, [
453            'year' => $dateTime->format('Y'),
454            'month' => $dateTime->format('m'),
455            'day' => $dateTime->format('d'),
456            'scopeID' => $scope->id,
457        ]);
458        return $list;
459    }
460
461    public function writeOptimizedSlotTables()
462    {
463        $status = true;
464        $status = ($status && $this->perform(Query\Slot::QUERY_OPTIMIZE_SLOT));
465        $status = ($status && $this->perform(Query\Slot::QUERY_OPTIMIZE_SLOT_HIERA));
466        $status = ($status && $this->perform(Query\Slot::QUERY_OPTIMIZE_SLOT_PROCESS));
467        $status = ($status && $this->perform(Query\Slot::QUERY_OPTIMIZE_PROCESS));
468        return $status;
469    }
470
471    private function getLastGeneratedSlotDate(AvailabilityEntity $availability)
472    {
473        $date = '1970-01-01 12:00';
474        $last = $this->fetchRow(
475            Query\Slot::QUERY_LAST_IN_AVAILABILITY,
476            [
477                'availabilityID' => $availability->id,
478            ]
479        );
480
481        if (isset($last['dateString'])) {
482            $date = $last['dateString'];
483        }
484
485        return new \DateTimeImmutable($date . \BO\Zmsdb\Connection\Select::$connectionTimezone);
486    }
487}