Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.19% covered (warning)
76.19%
96 / 126
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
OverallCalendar
76.19% covered (warning)
76.19%
96 / 126
28.57% covered (danger)
28.57%
2 / 7
15.28
0.00% covered (danger)
0.00%
0 / 1
 insertSlotsBulk
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 cancelAvailability
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 purgeMissingAvailabilityByScope
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 deleteOlderThan
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 book
90.24% covered (success)
90.24%
74 / 82
0.00% covered (danger)
0.00%
0 / 1
3.01
 unbook
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 readSlots
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace BO\Zmsdb;
4
5use BO\Zmsdb\Query\OverallCalendar as Calender;
6use DateInterval;
7use DateTimeImmutable;
8use DateTimeInterface;
9
10class OverallCalendar extends Base
11{
12    public function insertSlotsBulk(array $rows): void
13    {
14        if (!$rows) {
15            return;
16        }
17
18        $placeholders = rtrim(str_repeat('(?,?,?,?,?),', count($rows)), ',');
19        $sql = sprintf(Query\OverallCalendar::UPSERT_MULTI, $placeholders);
20
21        $params = [];
22        foreach ($rows as $r) {
23            $params[] = $r[0];
24            $params[] = $r[1];
25            $params[] = $r[2]->format('Y-m-d H:i:s');
26            $params[] = (int)$r[3];
27            $params[] = $r[4] ?? 'free';
28        }
29        $this->perform($sql, $params);
30    }
31
32    public function cancelAvailability(int $scopeId, int $availabilityId): void
33    {
34        $this->perform(Calender::CANCEL_AVAILABILITY, [
35            'scope_id' => $scopeId,
36            'availability_id' => $availabilityId,
37        ]);
38    }
39
40    public function purgeMissingAvailabilityByScope(
41        \DateTimeInterface $dateTime,
42        int $scopeId
43    ): bool {
44        return (bool) $this->perform(
45            Query\OverallCalendar::PURGE_MISSING_AVAIL_BY_SCOPE,
46            [
47                'dateString' => $dateTime->format('Y-m-d'),
48                'scopeID'    => $scopeId,
49            ]
50        );
51    }
52
53
54    public function deleteOlderThan(DateTimeInterface $date): bool
55    {
56        return (bool) $this->perform(Calender::DELETE_ALL_BEFORE, [
57            'threshold' => $date->format('Y-m-d 00:00:00'),
58        ]);
59    }
60
61    public function book(int $scopeId, string $startTime, int $processId, int $slotUnits): void
62    {
63        $start = new DateTimeImmutable($startTime);
64        $end   = $start->add(new DateInterval('PT' . ($slotUnits * 5) . 'M'));
65
66        $windowBefore = $this->fetchRow('
67            SELECT
68              SUM(status="free")      AS free_cnt,
69              SUM(status="cancelled") AS cancelled_cnt,
70              SUM(status="termin")    AS termin_cnt,
71              COUNT(DISTINCT availability_id) AS availability_ids
72            FROM gesamtkalender
73            WHERE scope_id=:scope AND time>=:start AND time<:end
74        ', [
75            'scope' => $scopeId,
76            'start' => $start->format('Y-m-d H:i:s'),
77            'end'   => $end  ->format('Y-m-d H:i:s'),
78        ]) ?? ['free_cnt' => 0,'cancelled_cnt' => 0,'termin_cnt' => 0,'availability_ids' => 0];
79
80        $availabilityDetails = $this->fetchAll('
81            SELECT DISTINCT
82                   g.availability_id,
83                   a.OeffnungszeitID,
84                   a.Startdatum, a.Endedatum,
85                   a.Anfangszeit, a.Terminanfangszeit,
86                   a.Endzeit, a.Terminendzeit,
87                   a.Timeslot,
88                   a.Anzahlarbeitsplaetze,
89                   a.Anzahlterminarbeitsplaetze
90            FROM gesamtkalender g
91            LEFT JOIN oeffnungszeit a ON a.OeffnungszeitID = g.availability_id
92            WHERE g.scope_id=:scope AND g.time>=:start AND g.time<:end
93        ', [
94            'scope' => $scopeId,
95            'start' => $start->format('Y-m-d H:i:s'),
96            'end'   => $end  ->format('Y-m-d H:i:s'),
97        ]);
98
99        $recentCancelled = (int)$this->fetchValue('
100            SELECT COUNT(*) FROM gesamtkalender
101             WHERE scope_id=:scope AND time>=:start AND time<:end
102               AND status="cancelled" AND updated_at > (NOW() - INTERVAL 2 MINUTE)
103        ', [
104            'scope' => $scopeId,
105            'start' => $start->format('Y-m-d H:i:s'),
106            'end'   => $end  ->format('Y-m-d H:i:s'),
107        ]);
108
109        \App::$log->info('calendar.book.attempt', [
110            'scope_id'        => $scopeId,
111            'process_id'      => $processId,
112            'window'          => ['from' => $start->format('Y-m-d H:i:s'), 'until' => $end->format('Y-m-d H:i:s')],
113            'slot_units'      => $slotUnits,
114            'window_before'   => $windowBefore,
115            'availability'    => $availabilityDetails,
116            'recent_cancelled' => $recentCancelled,
117        ]);
118
119        $seat = $this->fetchValue(Calender::FIND_FREE_SEAT, [
120            'scope' => $scopeId,
121            'start' => $start->format('Y-m-d H:i:s'),
122            'end'   => $end  ->format('Y-m-d H:i:s'),
123            'units' => $slotUnits,
124        ]);
125
126        if (!$seat) {
127            \App::$log->warning('calendar.book.no_seat', [
128                'scope_id'        => $scopeId,
129                'process_id'      => $processId,
130                'window'          => ['from' => $start->format('Y-m-d H:i:s'), 'until' => $end->format('Y-m-d H:i:s')],
131                'slot_units'      => $slotUnits,
132                'window_before'   => $windowBefore,
133                'recent_cancelled' => $recentCancelled,
134            ]);
135            return;
136        }
137
138        try {
139            $this->perform(Calender::BLOCK_SEAT_RANGE, [
140                'pid'   => $processId,
141                'units' => $slotUnits,
142                'scope' => $scopeId,
143                'seat'  => $seat,
144                'start' => $start->format('Y-m-d H:i:s'),
145                'end'   => $end  ->format('Y-m-d H:i:s'),
146            ]);
147        } catch (\PDOException $e) {
148            \App::$log->critical('calendar.book.update_failed', [
149                'scope_id'   => $scopeId,
150                'process_id' => $processId,
151                'seat'       => $seat,
152                'error'      => $e->getMessage()
153            ]);
154            throw $e;
155        }
156
157        $windowAfter = $this->fetchRow('
158            SELECT
159              SUM(status="free")      AS free_cnt,
160              SUM(status="cancelled") AS cancelled_cnt,
161              SUM(status="termin")    AS termin_cnt
162            FROM gesamtkalender
163            WHERE scope_id=:scope AND time>=:start AND time<:end
164        ', [
165            'scope' => $scopeId,
166            'start' => $start->format('Y-m-d H:i:s'),
167            'end'   => $end  ->format('Y-m-d H:i:s'),
168        ]) ?? ['free_cnt' => 0,'cancelled_cnt' => 0,'termin_cnt' => 0];
169
170        $terminByPid = (int)$this->fetchValue('
171            SELECT COUNT(*) FROM gesamtkalender
172             WHERE scope_id=:scope AND time>=:start AND time<:end
173               AND status="termin" AND process_id=:pid
174        ', [
175            'scope' => $scopeId,
176            'start' => $start->format('Y-m-d H:i:s'),
177            'end'   => $end  ->format('Y-m-d H:i:s'),
178            'pid'   => $processId,
179        ]);
180
181        \App::$log->info('calendar.book.result', [
182            'scope_id'       => $scopeId,
183            'process_id'     => $processId,
184            'seat'           => $seat,
185            'window_after'   => $windowAfter,
186            'termin_by_pid'  => $terminByPid,
187            'complete_chain' => ($terminByPid === $slotUnits),
188        ]);
189    }
190
191    public function unbook(int $scopeId, int $processId): void
192    {
193        $this->perform(Calender::UNBOOK_PROCESS, [
194            'scope_id' => $scopeId,
195            'process_id' => $processId,
196        ]);
197    }
198
199    public function readSlots(
200        array $scopeIds,
201        string $from,
202        string $until,
203        ?string $updatedAfter = null
204    ): array {
205        if (empty($scopeIds)) {
206            return [];
207        }
208
209        $in_list = implode(',', array_map('intval', $scopeIds));
210        $until = (new \DateTime($until))->modify('+1 day')->format('Y-m-d');
211
212        if ($updatedAfter === null) {
213            $sql = sprintf(Calender::SELECT_RANGE, $in_list);
214            $params = ['from' => $from, 'until' => $until];
215        } else {
216            $sql = sprintf(Calender::SELECT_RANGE_UPDATED, $in_list);
217            $params = [
218                'from'         => $from,
219                'until'        => $until,
220                'updatedAfter' => $updatedAfter
221            ];
222        }
223
224        return $this->fetchAll($sql, $params);
225    }
226}