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