PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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.10, at app/Repositories/AvailabilityRepository.php

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