Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
76.19% |
96 / 126 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
OverallCalendar | |
76.19% |
96 / 126 |
|
28.57% |
2 / 7 |
15.28 | |
0.00% |
0 / 1 |
insertSlotsBulk | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
3.01 | |||
cancelAvailability | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
purgeMissingAvailabilityByScope | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
deleteOlderThan | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
book | |
90.24% |
74 / 82 |
|
0.00% |
0 / 1 |
3.01 | |||
unbook | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
readSlots | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace BO\Zmsdb; |
4 | |
5 | use BO\Zmsdb\Query\OverallCalendar as Calender; |
6 | use DateInterval; |
7 | use DateTimeImmutable; |
8 | use DateTimeInterface; |
9 | |
10 | class 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 | } |