PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Repositories / AvailabilityRepository.php

AvailabilityRepository.php in Yatra – Travel Booking & Tour Operator Software 3.0.7, at app/Repositories/AvailabilityRepository.php

643 lines 24.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Repositories;
6
7 use Yatra\Models\Availability;
8 use Yatra\Database\Tables\TripAvailabilityDatesTable;
9
10 /**
11 * Availability Repository
12 * Handles database operations for trip availability dates
13 */
14 class AvailabilityRepository extends BaseRepository
15 {
16 /**
17 * Normalize optional latitude/longitude for storage (null if empty/invalid).
18 */
19 private function sanitizeCoordinate($value): ?string
20 {
21 if ($value === null || $value === '') {
22 return null;
23 }
24 if (is_numeric($value)) {
25 return (string) $value;
26 }
27
28 return null;
29 }
30
31 /**
32 * Clamp an alert threshold to the column's range (smallint unsigned: 0–65535)
33 * so out-of-range input can't abort the write under MySQL strict mode.
34 */
35 private function clampAlertThreshold($value): int
36 {
37 return max(0, min(65535, (int) $value));
38 }
39
40 /**
41 * Get table name
42 */
43 protected function getTableName(): string
44 {
45 return TripAvailabilityDatesTable::getTableName();
46 }
47
48 /**
49 * Find by ID
50 */
51 public function find(int $id, bool $includeDeleted = false): ?\stdClass
52 {
53 $result = parent::find($id, $includeDeleted);
54 return $result ? (object) Availability::fromArray((array) $result)->toArray() : null;
55 }
56
57 /**
58 * Find by ID and return Availability model
59 */
60 public function findModel(int $id): ?Availability
61 {
62 $result = parent::find($id);
63 return $result ? Availability::fromArray((array) $result) : null;
64 }
65
66 /**
67 * Find availability by trip ID and departure date
68 *
69 * @param int $tripId Trip ID
70 * @param string $departureDate Departure date (YYYY-MM-DD)
71 * @return object|null Availability object or null
72 */
73 public function findByTripIdAndDate(int $tripId, string $departureDate): ?object
74 {
75 $table = esc_sql($this->table);
76
77 $result = $this->wpdb->get_row($this->wpdb->prepare(
78 "SELECT * FROM `{$table}`
79 WHERE trip_id = %d
80 AND departure_date = %s
81 AND status IN ('available', 'limited')
82 LIMIT 1",
83 $tripId,
84 $departureDate
85 ));
86
87 return $result ?: null;
88 }
89
90 /**
91 * Find availability by trip ID, departure date, and optionally departure time.
92 * Supports day tours with multiple time slots on the same date.
93 *
94 * @param int $tripId Trip ID
95 * @param string $departureDate Departure date (YYYY-MM-DD)
96 * @param string|null $departureTime Departure time (HH:MM:SS or HH:MM)
97 * @return object|null Availability object or null
98 */
99 public function findByTripIdAndDateTime(int $tripId, string $departureDate, ?string $departureTime = null, bool $includeAnyStatus = false): ?object
100 {
101 $table = esc_sql($this->table);
102 $statusClause = $includeAnyStatus ? '1=1' : "status IN ('available', 'limited')";
103
104 if (!empty($departureTime)) {
105 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $statusClause is fixed safe SQL fragment
106 $result = $this->wpdb->get_row($this->wpdb->prepare(
107 "SELECT * FROM `{$table}`
108 WHERE trip_id = %d
109 AND departure_date = %s
110 AND departure_time = %s
111 AND {$statusClause}
112 LIMIT 1",
113 $tripId,
114 $departureDate,
115 $departureTime
116 ));
117 } else {
118 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
119 $result = $this->wpdb->get_row($this->wpdb->prepare(
120 "SELECT * FROM `{$table}`
121 WHERE trip_id = %d
122 AND departure_date = %s
123 AND {$statusClause}
124 LIMIT 1",
125 $tripId,
126 $departureDate
127 ));
128 }
129
130 return $result ?: null;
131 }
132
133 /**
134 * Find availability records by trip ID and departure date
135 *
136 * @param int $tripId Trip ID
137 * @param string $departureDate Departure date (YYYY-MM-DD)
138 * @return array Array of availability objects
139 */
140 public function findByTripAndDate(int $tripId, string $departureDate): array
141 {
142 $table = esc_sql($this->table);
143
144 $results = $this->wpdb->get_results($this->wpdb->prepare(
145 "SELECT * FROM `{$table}`
146 WHERE trip_id = %d
147 AND departure_date = %s
148 ORDER BY departure_time ASC",
149 $tripId,
150 $departureDate
151 ));
152
153 return $results ?: [];
154 }
155
156 /**
157 * Find availability records by trip ID within a date range
158 *
159 * @param int $tripId Trip ID
160 * @param string $fromDate Start date (YYYY-MM-DD)
161 * @param string $toDate End date (YYYY-MM-DD)
162 * @return array Array of availability objects
163 */
164 public function findByTripIdAndDateRange(int $tripId, string $fromDate, string $toDate): array
165 {
166 $table = esc_sql($this->table);
167
168 $results = $this->wpdb->get_results($this->wpdb->prepare(
169 "SELECT * FROM `{$table}`
170 WHERE trip_id = %d
171 AND departure_date >= %s
172 AND departure_date <= %s
173 ORDER BY departure_date ASC, departure_time ASC",
174 $tripId,
175 $fromDate,
176 $toDate
177 ));
178
179 return $results ?: [];
180 }
181
182 public function existsForTripDateTime(int $tripId, string $departureDate, ?string $departureTime): bool
183 {
184 $table = esc_sql($this->table);
185
186 if ($departureTime === null || $departureTime === '') {
187 $count = (int) $this->wpdb->get_var($this->wpdb->prepare(
188 "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time IS NULL",
189 $tripId,
190 $departureDate
191 ));
192 } else {
193 $count = (int) $this->wpdb->get_var($this->wpdb->prepare(
194 "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time = %s",
195 $tripId,
196 $departureDate,
197 $departureTime
198 ));
199 }
200
201 return $count > 0;
202 }
203
204 /**
205 * Find all by trip ID
206 */
207 public function findByTripId(int $tripId, array $filters = []): array
208 {
209 $table = esc_sql($this->table);
210 $where = ['trip_id = %d'];
211 $params = [$tripId];
212
213 // Status filter
214 if (!empty($filters['status']) && $filters['status'] !== 'all') {
215 $where[] = 'status = %s';
216 $params[] = $filters['status'];
217 }
218
219 // Month filter
220 if (!empty($filters['month']) && $filters['month'] !== 'all') {
221 $where[] = 'YEAR(departure_date) = %d AND MONTH(departure_date) = %d';
222 [$year, $month] = explode('-', $filters['month']);
223 $params[] = (int) $year;
224 $params[] = (int) $month;
225 }
226
227 // Search filter
228 if (!empty($filters['search'])) {
229 $where[] = '(departure_date LIKE %s OR arrival_date LIKE %s OR from_location LIKE %s OR to_location LIKE %s)';
230 $search = '%' . $this->wpdb->esc_like($filters['search']) . '%';
231 $params[] = $search;
232 $params[] = $search;
233 $params[] = $search;
234 $params[] = $search;
235 }
236
237 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
238 $query .= " ORDER BY departure_date ASC, departure_time ASC";
239
240 if (!empty($filters['per_page'])) {
241 $perPage = (int) $filters['per_page'];
242 $page = max(1, (int) ($filters['page'] ?? 1));
243 $offset = ($page - 1) * $perPage;
244 $query .= " LIMIT %d OFFSET %d";
245 $params[] = $perPage;
246 $params[] = $offset;
247 }
248
249 $results = $this->wpdb->get_results(
250 $this->wpdb->prepare($query, $params),
251 ARRAY_A
252 );
253
254 return array_map(function ($row) {
255 return Availability::fromArray($row);
256 }, $results ?: []);
257 }
258
259 /**
260 * Count by trip ID
261 */
262 public function countByTripId(int $tripId, array $filters = []): int
263 {
264 $table = esc_sql($this->table);
265 $where = ['trip_id = %d'];
266 $params = [$tripId];
267
268 // Status filter
269 if (!empty($filters['status']) && $filters['status'] !== 'all') {
270 $where[] = 'status = %s';
271 $params[] = $filters['status'];
272 }
273
274 // Month filter
275 if (!empty($filters['month']) && $filters['month'] !== 'all') {
276 $where[] = 'YEAR(departure_date) = %d AND MONTH(departure_date) = %d';
277 [$year, $month] = explode('-', $filters['month']);
278 $params[] = (int) $year;
279 $params[] = (int) $month;
280 }
281
282 // Search filter
283 if (!empty($filters['search'])) {
284 $where[] = '(departure_date LIKE %s OR arrival_date LIKE %s OR from_location LIKE %s OR to_location LIKE %s)';
285 $search = '%' . $this->wpdb->esc_like($filters['search']) . '%';
286 $params[] = $search;
287 $params[] = $search;
288 $params[] = $search;
289 $params[] = $search;
290 }
291
292 $query = "SELECT COUNT(*) FROM `{$table}` WHERE " . implode(' AND ', $where);
293
294 return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $params));
295 }
296
297 /**
298 * Create availability date
299 */
300 public function create(array $data): int
301 {
302 $table = esc_sql($this->table);
303
304 $insertData = [
305 'trip_id' => (int) ($data['trip_id'] ?? 0),
306 'departure_date' => sanitize_text_field($data['departure_date'] ?? ''),
307 'arrival_date' => !empty($data['arrival_date']) ? sanitize_text_field($data['arrival_date']) : null,
308 'return_date' => !empty($data['return_date']) ? sanitize_text_field($data['return_date']) : null,
309 'departure_time' => !empty($data['departure_time']) ? sanitize_text_field($data['departure_time']) : null,
310 'arrival_time' => !empty($data['arrival_time']) ? sanitize_text_field($data['arrival_time']) : null,
311 'seats_total' => (int) ($data['seats_total'] ?? 0),
312 'seats_available' => (int) ($data['seats_available'] ?? ($data['seats_total'] ?? 0)),
313 'seats_reserved' => (int) ($data['seats_reserved'] ?? 0),
314 'seats_waitlist' => (int) ($data['seats_waitlist'] ?? 0),
315 'pricing_type' => sanitize_text_field($data['pricing_type'] ?? 'regular'),
316 'original_price' => !empty($data['original_price']) ? (float) $data['original_price'] : null,
317 'discounted_price' => !empty($data['discounted_price']) ? (float) $data['discounted_price'] : null,
318 'discount_percentage' => !empty($data['discount_percentage']) ? (float) $data['discount_percentage'] : null,
319 'price_types' => !empty($data['price_types']) ? (is_array($data['price_types']) ? wp_json_encode($data['price_types']) : $data['price_types']) : null,
320 'status' => sanitize_text_field($data['status'] ?? 'available'),
321 'from_location' => !empty($data['from_location']) ? sanitize_text_field($data['from_location']) : null,
322 'to_location' => !empty($data['to_location']) ? sanitize_text_field($data['to_location']) : null,
323 'from_latitude' => $this->sanitizeCoordinate($data['from_latitude'] ?? null),
324 'from_longitude' => $this->sanitizeCoordinate($data['from_longitude'] ?? null),
325 'to_latitude' => $this->sanitizeCoordinate($data['to_latitude'] ?? null),
326 'to_longitude' => $this->sanitizeCoordinate($data['to_longitude'] ?? null),
327 'special_notes' => !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null,
328 'cutoff_date' => !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null,
329 'cutoff_hours' => (int) ($data['cutoff_hours'] ?? 24),
330 'is_blocked' => !empty($data['is_blocked']) ? 1 : 0,
331 'block_reason' => !empty($data['block_reason']) ? mb_substr(sanitize_textarea_field($data['block_reason']), 0, 255) : null,
332 'alert_threshold' => (isset($data['alert_threshold']) && $data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null,
333 ];
334
335 // Calculate discount percentage if not provided
336 if (!empty($insertData['original_price']) && !empty($insertData['discounted_price']) && empty($data['discount_percentage'])) {
337 $insertData['discount_percentage'] = round((($insertData['original_price'] - $insertData['discounted_price']) / $insertData['original_price']) * 100, 2);
338 }
339
340 $this->wpdb->insert($table, $insertData, [
341 '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d',
342 '%s', '%f', '%f', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d',
343 '%d', '%s', '%d',
344 ]);
345
346 return $this->wpdb->insert_id;
347 }
348
349 /**
350 * Update availability date
351 */
352 public function update(int $id, array $data): bool
353 {
354 $table = esc_sql($this->table);
355
356 $updateData = [];
357
358 if (isset($data['trip_id'])) $updateData['trip_id'] = (int) $data['trip_id'];
359 if (isset($data['departure_date'])) $updateData['departure_date'] = sanitize_text_field($data['departure_date']);
360 if (isset($data['arrival_date'])) $updateData['arrival_date'] = !empty($data['arrival_date']) ? sanitize_text_field($data['arrival_date']) : null;
361 if (isset($data['return_date'])) $updateData['return_date'] = !empty($data['return_date']) ? sanitize_text_field($data['return_date']) : null;
362 if (isset($data['departure_time'])) $updateData['departure_time'] = !empty($data['departure_time']) ? sanitize_text_field($data['departure_time']) : null;
363 if (isset($data['arrival_time'])) $updateData['arrival_time'] = !empty($data['arrival_time']) ? sanitize_text_field($data['arrival_time']) : null;
364 if (isset($data['seats_total'])) $updateData['seats_total'] = (int) $data['seats_total'];
365 if (isset($data['seats_available'])) $updateData['seats_available'] = (int) $data['seats_available'];
366 if (isset($data['seats_reserved'])) $updateData['seats_reserved'] = (int) $data['seats_reserved'];
367 if (isset($data['seats_waitlist'])) $updateData['seats_waitlist'] = (int) $data['seats_waitlist'];
368 if (isset($data['pricing_type'])) $updateData['pricing_type'] = sanitize_text_field($data['pricing_type']);
369 if (isset($data['original_price'])) $updateData['original_price'] = !empty($data['original_price']) ? (float) $data['original_price'] : null;
370 if (isset($data['discounted_price'])) $updateData['discounted_price'] = !empty($data['discounted_price']) ? (float) $data['discounted_price'] : null;
371 if (isset($data['discount_percentage'])) $updateData['discount_percentage'] = !empty($data['discount_percentage']) ? (float) $data['discount_percentage'] : null;
372 if (isset($data['price_types'])) $updateData['price_types'] = !empty($data['price_types']) ? (is_array($data['price_types']) ? wp_json_encode($data['price_types']) : $data['price_types']) : null;
373 if (isset($data['status'])) $updateData['status'] = sanitize_text_field($data['status']);
374 if (isset($data['from_location'])) $updateData['from_location'] = !empty($data['from_location']) ? sanitize_text_field($data['from_location']) : null;
375 if (isset($data['to_location'])) $updateData['to_location'] = !empty($data['to_location']) ? sanitize_text_field($data['to_location']) : null;
376 if (array_key_exists('from_latitude', $data)) {
377 $updateData['from_latitude'] = $this->sanitizeCoordinate($data['from_latitude']);
378 }
379 if (array_key_exists('from_longitude', $data)) {
380 $updateData['from_longitude'] = $this->sanitizeCoordinate($data['from_longitude']);
381 }
382 if (array_key_exists('to_latitude', $data)) {
383 $updateData['to_latitude'] = $this->sanitizeCoordinate($data['to_latitude']);
384 }
385 if (array_key_exists('to_longitude', $data)) {
386 $updateData['to_longitude'] = $this->sanitizeCoordinate($data['to_longitude']);
387 }
388 if (isset($data['special_notes'])) $updateData['special_notes'] = !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null;
389 if (isset($data['cutoff_date'])) $updateData['cutoff_date'] = !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null;
390 if (isset($data['cutoff_hours'])) $updateData['cutoff_hours'] = (int) $data['cutoff_hours'];
391 // array_key_exists (not isset) so an explicit null from the form — e.g.
392 // clearing the block reason / threshold when a date is unblocked — is
393 // honored instead of silently skipped (isset(null) === false).
394 if (array_key_exists('is_blocked', $data)) $updateData['is_blocked'] = !empty($data['is_blocked']) ? 1 : 0;
395 if (array_key_exists('block_reason', $data)) $updateData['block_reason'] = !empty($data['block_reason']) ? mb_substr(sanitize_textarea_field($data['block_reason']), 0, 255) : null;
396 if (array_key_exists('alert_threshold', $data)) $updateData['alert_threshold'] = ($data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null;
397
398 // Calculate discount percentage if not provided
399 if (!empty($updateData['original_price']) && !empty($updateData['discounted_price']) && empty($updateData['discount_percentage'])) {
400 $updateData['discount_percentage'] = round((($updateData['original_price'] - $updateData['discounted_price']) / $updateData['original_price']) * 100, 2);
401 }
402
403 if (empty($updateData)) {
404 return false;
405 }
406
407 $formats = [];
408 foreach ($updateData as $value) {
409 if (is_int($value)) {
410 $formats[] = '%d';
411 } elseif (is_float($value)) {
412 $formats[] = '%f';
413 } else {
414 $formats[] = '%s';
415 }
416 }
417
418 return (bool) $this->wpdb->update(
419 $table,
420 $updateData,
421 ['id' => $id],
422 $formats,
423 ['%d']
424 );
425 }
426
427 /**
428 * Delete availability date
429 */
430 public function delete(int $id): bool
431 {
432 $table = esc_sql($this->table);
433 return (bool) $this->wpdb->delete($table, ['id' => $id], ['%d']);
434 }
435
436 /**
437 * Atomically adjust seats_waitlist (negative delta when promoting from waitlist).
438 */
439 public function incrementSeatsWaitlist(int $id, int $delta): void
440 {
441 if ($id <= 0 || $delta === 0) {
442 return;
443 }
444
445 $table = esc_sql($this->table);
446 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
447 $this->wpdb->query($this->wpdb->prepare(
448 "UPDATE `{$table}` SET seats_waitlist = GREATEST(0, COALESCE(seats_waitlist, 0) + %d) WHERE id = %d",
449 $delta,
450 $id
451 ));
452 }
453
454 /**
455 * Check if table supports soft delete
456 */
457 protected function hasSoftDelete(): bool
458 {
459 return false; // Availability table doesn't have soft delete
460 }
461
462 /**
463 * Update pricing type for all availability dates of a trip
464 *
465 * @param int $tripId Trip ID
466 * @param string $pricingType Pricing type
467 * @return int Number of rows updated
468 */
469 public function updatePricingTypeByTripId(int $tripId, string $pricingType): int
470 {
471 $table = esc_sql($this->table);
472 return (int) $this->wpdb->update(
473 $table,
474 ['pricing_type' => $pricingType],
475 ['trip_id' => $tripId],
476 ['%s'],
477 ['%d']
478 );
479 }
480
481 /**
482 * Clear price types for all availability dates of a trip
483 *
484 * @param int $tripId Trip ID
485 * @return int Number of rows updated
486 */
487 public function clearPriceTypesByTripId(int $tripId): int
488 {
489 $table = esc_sql($this->table);
490 return (int) $this->wpdb->query(
491 $this->wpdb->prepare(
492 "UPDATE {$table}
493 SET price_types = NULL
494 WHERE trip_id = %d",
495 $tripId
496 )
497 );
498 }
499
500 /**
501 * Clear traveler pricing from availability dates for a trip
502 *
503 * @param int $tripId Trip ID
504 * @return int Number of rows updated
505 */
506 public function clearTravelerPricingByTripId(int $tripId): int
507 {
508 $table = esc_sql($this->table);
509 return (int) $this->wpdb->query(
510 $this->wpdb->prepare(
511 "UPDATE {$table}
512 SET price_types = NULL
513 WHERE trip_id = %d",
514 $tripId
515 )
516 );
517 }
518
519 /**
520 * Get specific dates for a trip within a date range
521 *
522 * @param int $tripId Trip ID
523 * @param string $startDate Start date (Y-m-d)
524 * @param string $endDate End date (Y-m-d)
525 * @return array Array of specific date records
526 */
527 public function getDatesForTrip(int $tripId, string $startDate, string $endDate): array
528 {
529 $table = esc_sql($this->table);
530
531 $sql = "SELECT * FROM `{$table}`
532 WHERE `trip_id` = %d
533 AND `date` BETWEEN %s AND %s
534 ORDER BY `date` ASC";
535
536 $query = $this->wpdb->prepare($sql, $tripId, $startDate, $endDate);
537 return $this->wpdb->get_results($query) ?: [];
538 }
539
540 /**
541 * Get specific date for a trip on a particular date
542 *
543 * @param int $tripId Trip ID
544 * @param string $date Date (Y-m-d)
545 * @return object|null Specific date record or null
546 */
547 public function getDateForTrip(int $tripId, string $date): ?object
548 {
549 $table = esc_sql($this->table);
550
551 $sql = "SELECT * FROM `{$table}`
552 WHERE `trip_id` = %d
553 AND `date` = %s
554 LIMIT 1";
555
556 $query = $this->wpdb->prepare($sql, $tripId, $date);
557 $result = $this->wpdb->get_row($query);
558
559 return $result ?: null;
560 }
561
562 /**
563 * Get available dates for a trip within a date range
564 *
565 * @param int $tripId Trip ID
566 * @param string $startDate Start date (Y-m-d)
567 * @param string $endDate End date (Y-m-d)
568 * @return array Array of available dates
569 */
570 public function getAvailableDates(int $tripId, string $startDate, string $endDate): array
571 {
572 $table = esc_sql($this->table);
573
574 $sql = "SELECT * FROM `{$table}`
575 WHERE `trip_id` = %d
576 AND `date` BETWEEN %s AND %s
577 AND `status` = 'available'
578 AND (`max_bookings` IS NULL OR `current_bookings` < `max_bookings`)
579 ORDER BY `date` ASC";
580
581 $query = $this->wpdb->prepare($sql, $tripId, $startDate, $endDate);
582 return $this->wpdb->get_results($query) ?: [];
583 }
584
585 /**
586 * Update current bookings count for a specific date
587 *
588 * @param int $id Specific date record ID
589 * @param int $bookingCount New booking count
590 * @return bool Success status
591 */
592 public function updateBookingCount(int $id, int $bookingCount): bool
593 {
594 $table = esc_sql($this->table);
595
596 $sql = "UPDATE `{$table}`
597 SET `current_bookings` = %d, `updated_at` = NOW()
598 WHERE `id` = %d";
599
600 $query = $this->wpdb->prepare($sql, $bookingCount, $id);
601 return (bool) $this->wpdb->query($query);
602 }
603
604 /**
605 * Increment booking count for a specific date
606 *
607 * @param int $id Specific date record ID
608 * @param int $increment Number to increment by (default: 1)
609 * @return bool Success status
610 */
611 public function incrementBookingCount(int $id, int $increment = 1): bool
612 {
613 $table = esc_sql($this->table);
614
615 $sql = "UPDATE `{$table}`
616 SET `current_bookings` = `current_bookings` + %d, `updated_at` = NOW()
617 WHERE `id` = %d";
618
619 $query = $this->wpdb->prepare($sql, $increment, $id);
620 return (bool) $this->wpdb->query($query);
621 }
622
623 /**
624 * Decrement booking count for a specific date
625 *
626 * @param int $id Specific date record ID
627 * @param int $decrement Number to decrement by (default: 1)
628 * @return bool Success status
629 */
630 public function decrementBookingCount(int $id, int $decrement = 1): bool
631 {
632 $table = esc_sql($this->table);
633
634 $sql = "UPDATE `{$table}`
635 SET `current_bookings` = GREATEST(0, `current_bookings` - %d), `updated_at` = NOW()
636 WHERE `id` = %d";
637
638 $query = $this->wpdb->prepare($sql, $decrement, $id);
639 return (bool) $this->wpdb->query($query);
640 }
641 }
642
643