PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 3.0.16 3.0.15 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 All 85 releases
yatra / app / Repositories / AvailabilityRepository.php

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

664 lines 26.0 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, ?int $excludeId = null): bool
189 {
190 $table = esc_sql($this->table);
191
192 // Optional so update() can ignore the row it is editing; omitted, behaviour
193 // is exactly as before for existing callers.
194 $exclude = $excludeId !== null ? $this->wpdb->prepare(' AND id <> %d', $excludeId) : '';
195
196 if ($departureTime === null || $departureTime === '') {
197 $count = (int) $this->wpdb->get_var($this->wpdb->prepare(
198 "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time IS NULL{$exclude}",
199 $tripId,
200 $departureDate
201 ));
202 } else {
203 $count = (int) $this->wpdb->get_var($this->wpdb->prepare(
204 "SELECT COUNT(*) FROM `{$table}` WHERE trip_id = %d AND departure_date = %s AND departure_time = %s{$exclude}",
205 $tripId,
206 $departureDate,
207 $departureTime
208 ));
209 }
210
211 return $count > 0;
212 }
213
214 /**
215 * Find all by trip ID
216 */
217 public function findByTripId(int $tripId, array $filters = []): array
218 {
219 $table = esc_sql($this->table);
220 $where = ['trip_id = %d'];
221 $params = [$tripId];
222
223 // Status filter
224 if (!empty($filters['status']) && $filters['status'] !== 'all') {
225 $where[] = 'status = %s';
226 $params[] = $filters['status'];
227 }
228
229 // Month filter
230 if (!empty($filters['month']) && $filters['month'] !== 'all') {
231 $where[] = 'YEAR(departure_date) = %d AND MONTH(departure_date) = %d';
232 [$year, $month] = explode('-', $filters['month']);
233 $params[] = (int) $year;
234 $params[] = (int) $month;
235 }
236
237 // Search filter
238 if (!empty($filters['search'])) {
239 $where[] = '(departure_date LIKE %s OR arrival_date LIKE %s OR from_location LIKE %s OR to_location LIKE %s)';
240 $search = '%' . $this->wpdb->esc_like($filters['search']) . '%';
241 $params[] = $search;
242 $params[] = $search;
243 $params[] = $search;
244 $params[] = $search;
245 }
246
247 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
248 $query .= " ORDER BY departure_date ASC, departure_time ASC";
249
250 if (!empty($filters['per_page'])) {
251 $perPage = (int) $filters['per_page'];
252 $page = max(1, (int) ($filters['page'] ?? 1));
253 $offset = ($page - 1) * $perPage;
254 $query .= " LIMIT %d OFFSET %d";
255 $params[] = $perPage;
256 $params[] = $offset;
257 }
258
259 $results = $this->wpdb->get_results(
260 $this->wpdb->prepare($query, $params),
261 ARRAY_A
262 );
263
264 return array_map(function ($row) {
265 return Availability::fromArray($row);
266 }, $results ?: []);
267 }
268
269 /**
270 * Count by trip ID
271 */
272 public function countByTripId(int $tripId, array $filters = []): int
273 {
274 $table = esc_sql($this->table);
275 $where = ['trip_id = %d'];
276 $params = [$tripId];
277
278 // Status filter
279 if (!empty($filters['status']) && $filters['status'] !== 'all') {
280 $where[] = 'status = %s';
281 $params[] = $filters['status'];
282 }
283
284 // Month filter
285 if (!empty($filters['month']) && $filters['month'] !== 'all') {
286 $where[] = 'YEAR(departure_date) = %d AND MONTH(departure_date) = %d';
287 [$year, $month] = explode('-', $filters['month']);
288 $params[] = (int) $year;
289 $params[] = (int) $month;
290 }
291
292 // Search filter
293 if (!empty($filters['search'])) {
294 $where[] = '(departure_date LIKE %s OR arrival_date LIKE %s OR from_location LIKE %s OR to_location LIKE %s)';
295 $search = '%' . $this->wpdb->esc_like($filters['search']) . '%';
296 $params[] = $search;
297 $params[] = $search;
298 $params[] = $search;
299 $params[] = $search;
300 }
301
302 $query = "SELECT COUNT(*) FROM `{$table}` WHERE " . implode(' AND ', $where);
303
304 return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $params));
305 }
306
307 /**
308 * Create availability date
309 */
310 public function create(array $data): int
311 {
312 $table = esc_sql($this->table);
313
314 $insertData = [
315 'trip_id' => (int) ($data['trip_id'] ?? 0),
316 'departure_date' => sanitize_text_field($data['departure_date'] ?? ''),
317 'arrival_date' => !empty($data['arrival_date']) ? sanitize_text_field($data['arrival_date']) : null,
318 'return_date' => !empty($data['return_date']) ? sanitize_text_field($data['return_date']) : null,
319 'departure_time' => !empty($data['departure_time']) ? sanitize_text_field($data['departure_time']) : null,
320 'arrival_time' => !empty($data['arrival_time']) ? sanitize_text_field($data['arrival_time']) : null,
321 'seats_total' => (int) ($data['seats_total'] ?? 0),
322 'seats_available' => (int) ($data['seats_available'] ?? ($data['seats_total'] ?? 0)),
323 'seats_reserved' => (int) ($data['seats_reserved'] ?? 0),
324 'seats_waitlist' => (int) ($data['seats_waitlist'] ?? 0),
325 'pricing_type' => sanitize_text_field($data['pricing_type'] ?? 'regular'),
326 'original_price' => !empty($data['original_price']) ? (float) $data['original_price'] : null,
327 'discounted_price' => !empty($data['discounted_price']) ? (float) $data['discounted_price'] : null,
328 'discount_percentage' => !empty($data['discount_percentage']) ? (float) $data['discount_percentage'] : null,
329 'price_types' => !empty($data['price_types']) ? (is_array($data['price_types']) ? wp_json_encode($data['price_types']) : $data['price_types']) : null,
330 'status' => sanitize_text_field($data['status'] ?? 'available'),
331 'from_location' => !empty($data['from_location']) ? sanitize_text_field($data['from_location']) : null,
332 'to_location' => !empty($data['to_location']) ? sanitize_text_field($data['to_location']) : null,
333 'from_latitude' => $this->sanitizeCoordinate($data['from_latitude'] ?? null),
334 'from_longitude' => $this->sanitizeCoordinate($data['from_longitude'] ?? null),
335 'to_latitude' => $this->sanitizeCoordinate($data['to_latitude'] ?? null),
336 'to_longitude' => $this->sanitizeCoordinate($data['to_longitude'] ?? null),
337 'special_notes' => !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null,
338 'cutoff_date' => !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null,
339 'cutoff_hours' => (int) ($data['cutoff_hours'] ?? 24),
340 'is_blocked' => !empty($data['is_blocked']) ? 1 : 0,
341 'block_reason' => !empty($data['block_reason']) ? mb_substr(sanitize_textarea_field($data['block_reason']), 0, 255) : null,
342 'alert_threshold' => (isset($data['alert_threshold']) && $data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null,
343 ];
344
345 // Calculate discount percentage if not provided
346 if (!empty($insertData['original_price']) && !empty($insertData['discounted_price']) && empty($data['discount_percentage'])) {
347 $insertData['discount_percentage'] = round((($insertData['original_price'] - $insertData['discounted_price']) / $insertData['original_price']) * 100, 2);
348 }
349
350 $inserted = $this->wpdb->insert($table, $insertData, [
351 '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d',
352 '%s', '%f', '%f', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d',
353 '%d', '%s', '%d',
354 ]);
355
356 // A rejected insert used to be swallowed: insert_id stays 0, the caller
357 // looks up row 0, gets null, and trips its own return type with a fatal
358 // TypeError. Fail loudly instead so the caller can report something useful.
359 if ($inserted === false) {
360 throw new \RuntimeException(
361 $this->wpdb->last_error !== ''
362 ? $this->wpdb->last_error
363 : 'Could not save the availability date.'
364 );
365 }
366
367 return (int) $this->wpdb->insert_id;
368 }
369
370 /**
371 * Update availability date
372 */
373 public function update(int $id, array $data): bool
374 {
375 $table = esc_sql($this->table);
376
377 $updateData = [];
378
379 if (isset($data['trip_id'])) $updateData['trip_id'] = (int) $data['trip_id'];
380 if (isset($data['departure_date'])) $updateData['departure_date'] = sanitize_text_field($data['departure_date']);
381 if (isset($data['arrival_date'])) $updateData['arrival_date'] = !empty($data['arrival_date']) ? sanitize_text_field($data['arrival_date']) : null;
382 if (isset($data['return_date'])) $updateData['return_date'] = !empty($data['return_date']) ? sanitize_text_field($data['return_date']) : null;
383 if (isset($data['departure_time'])) $updateData['departure_time'] = !empty($data['departure_time']) ? sanitize_text_field($data['departure_time']) : null;
384 if (isset($data['arrival_time'])) $updateData['arrival_time'] = !empty($data['arrival_time']) ? sanitize_text_field($data['arrival_time']) : null;
385 if (isset($data['seats_total'])) $updateData['seats_total'] = (int) $data['seats_total'];
386 if (isset($data['seats_available'])) $updateData['seats_available'] = (int) $data['seats_available'];
387 if (isset($data['seats_reserved'])) $updateData['seats_reserved'] = (int) $data['seats_reserved'];
388 if (isset($data['seats_waitlist'])) $updateData['seats_waitlist'] = (int) $data['seats_waitlist'];
389 if (isset($data['pricing_type'])) $updateData['pricing_type'] = sanitize_text_field($data['pricing_type']);
390 if (isset($data['original_price'])) $updateData['original_price'] = !empty($data['original_price']) ? (float) $data['original_price'] : null;
391 if (isset($data['discounted_price'])) $updateData['discounted_price'] = !empty($data['discounted_price']) ? (float) $data['discounted_price'] : null;
392 if (isset($data['discount_percentage'])) $updateData['discount_percentage'] = !empty($data['discount_percentage']) ? (float) $data['discount_percentage'] : null;
393 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;
394 if (isset($data['status'])) $updateData['status'] = sanitize_text_field($data['status']);
395 if (isset($data['from_location'])) $updateData['from_location'] = !empty($data['from_location']) ? sanitize_text_field($data['from_location']) : null;
396 if (isset($data['to_location'])) $updateData['to_location'] = !empty($data['to_location']) ? sanitize_text_field($data['to_location']) : null;
397 if (array_key_exists('from_latitude', $data)) {
398 $updateData['from_latitude'] = $this->sanitizeCoordinate($data['from_latitude']);
399 }
400 if (array_key_exists('from_longitude', $data)) {
401 $updateData['from_longitude'] = $this->sanitizeCoordinate($data['from_longitude']);
402 }
403 if (array_key_exists('to_latitude', $data)) {
404 $updateData['to_latitude'] = $this->sanitizeCoordinate($data['to_latitude']);
405 }
406 if (array_key_exists('to_longitude', $data)) {
407 $updateData['to_longitude'] = $this->sanitizeCoordinate($data['to_longitude']);
408 }
409 if (isset($data['special_notes'])) $updateData['special_notes'] = !empty($data['special_notes']) ? sanitize_textarea_field($data['special_notes']) : null;
410 if (isset($data['cutoff_date'])) $updateData['cutoff_date'] = !empty($data['cutoff_date']) ? sanitize_text_field($data['cutoff_date']) : null;
411 if (isset($data['cutoff_hours'])) $updateData['cutoff_hours'] = (int) $data['cutoff_hours'];
412 // array_key_exists (not isset) so an explicit null from the form — e.g.
413 // clearing the block reason / threshold when a date is unblocked — is
414 // honored instead of silently skipped (isset(null) === false).
415 if (array_key_exists('is_blocked', $data)) $updateData['is_blocked'] = !empty($data['is_blocked']) ? 1 : 0;
416 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;
417 if (array_key_exists('alert_threshold', $data)) $updateData['alert_threshold'] = ($data['alert_threshold'] !== '' && $data['alert_threshold'] !== null) ? $this->clampAlertThreshold($data['alert_threshold']) : null;
418
419 // Calculate discount percentage if not provided
420 if (!empty($updateData['original_price']) && !empty($updateData['discounted_price']) && empty($updateData['discount_percentage'])) {
421 $updateData['discount_percentage'] = round((($updateData['original_price'] - $updateData['discounted_price']) / $updateData['original_price']) * 100, 2);
422 }
423
424 if (empty($updateData)) {
425 return false;
426 }
427
428 $formats = [];
429 foreach ($updateData as $value) {
430 if (is_int($value)) {
431 $formats[] = '%d';
432 } elseif (is_float($value)) {
433 $formats[] = '%f';
434 } else {
435 $formats[] = '%s';
436 }
437 }
438
439 return (bool) $this->wpdb->update(
440 $table,
441 $updateData,
442 ['id' => $id],
443 $formats,
444 ['%d']
445 );
446 }
447
448 /**
449 * Delete availability date
450 */
451 public function delete(int $id): bool
452 {
453 $table = esc_sql($this->table);
454 return (bool) $this->wpdb->delete($table, ['id' => $id], ['%d']);
455 }
456
457 /**
458 * Atomically adjust seats_waitlist (negative delta when promoting from waitlist).
459 */
460 public function incrementSeatsWaitlist(int $id, int $delta): void
461 {
462 if ($id <= 0 || $delta === 0) {
463 return;
464 }
465
466 $table = esc_sql($this->table);
467 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
468 $this->wpdb->query($this->wpdb->prepare(
469 "UPDATE `{$table}` SET seats_waitlist = GREATEST(0, COALESCE(seats_waitlist, 0) + %d) WHERE id = %d",
470 $delta,
471 $id
472 ));
473 }
474
475 /**
476 * Check if table supports soft delete
477 */
478 protected function hasSoftDelete(): bool
479 {
480 return false; // Availability table doesn't have soft delete
481 }
482
483 /**
484 * Update pricing type for all availability dates of a trip
485 *
486 * @param int $tripId Trip ID
487 * @param string $pricingType Pricing type
488 * @return int Number of rows updated
489 */
490 public function updatePricingTypeByTripId(int $tripId, string $pricingType): int
491 {
492 $table = esc_sql($this->table);
493 return (int) $this->wpdb->update(
494 $table,
495 ['pricing_type' => $pricingType],
496 ['trip_id' => $tripId],
497 ['%s'],
498 ['%d']
499 );
500 }
501
502 /**
503 * Clear price types for all availability dates of a trip
504 *
505 * @param int $tripId Trip ID
506 * @return int Number of rows updated
507 */
508 public function clearPriceTypesByTripId(int $tripId): int
509 {
510 $table = esc_sql($this->table);
511 return (int) $this->wpdb->query(
512 $this->wpdb->prepare(
513 "UPDATE {$table}
514 SET price_types = NULL
515 WHERE trip_id = %d",
516 $tripId
517 )
518 );
519 }
520
521 /**
522 * Clear traveler pricing from availability dates for a trip
523 *
524 * @param int $tripId Trip ID
525 * @return int Number of rows updated
526 */
527 public function clearTravelerPricingByTripId(int $tripId): int
528 {
529 $table = esc_sql($this->table);
530 return (int) $this->wpdb->query(
531 $this->wpdb->prepare(
532 "UPDATE {$table}
533 SET price_types = NULL
534 WHERE trip_id = %d",
535 $tripId
536 )
537 );
538 }
539
540 /**
541 * Get specific dates for a trip within a date range
542 *
543 * @param int $tripId Trip ID
544 * @param string $startDate Start date (Y-m-d)
545 * @param string $endDate End date (Y-m-d)
546 * @return array Array of specific date records
547 */
548 public function getDatesForTrip(int $tripId, string $startDate, string $endDate): array
549 {
550 $table = esc_sql($this->table);
551
552 $sql = "SELECT * FROM `{$table}`
553 WHERE `trip_id` = %d
554 AND `date` BETWEEN %s AND %s
555 ORDER BY `date` ASC";
556
557 $query = $this->wpdb->prepare($sql, $tripId, $startDate, $endDate);
558 return $this->wpdb->get_results($query) ?: [];
559 }
560
561 /**
562 * Get specific date for a trip on a particular date
563 *
564 * @param int $tripId Trip ID
565 * @param string $date Date (Y-m-d)
566 * @return object|null Specific date record or null
567 */
568 public function getDateForTrip(int $tripId, string $date): ?object
569 {
570 $table = esc_sql($this->table);
571
572 $sql = "SELECT * FROM `{$table}`
573 WHERE `trip_id` = %d
574 AND `date` = %s
575 LIMIT 1";
576
577 $query = $this->wpdb->prepare($sql, $tripId, $date);
578 $result = $this->wpdb->get_row($query);
579
580 return $result ?: null;
581 }
582
583 /**
584 * Get available dates for a trip within a date range
585 *
586 * @param int $tripId Trip ID
587 * @param string $startDate Start date (Y-m-d)
588 * @param string $endDate End date (Y-m-d)
589 * @return array Array of available dates
590 */
591 public function getAvailableDates(int $tripId, string $startDate, string $endDate): array
592 {
593 $table = esc_sql($this->table);
594
595 $sql = "SELECT * FROM `{$table}`
596 WHERE `trip_id` = %d
597 AND `date` BETWEEN %s AND %s
598 AND `status` = 'available'
599 AND (`max_bookings` IS NULL OR `current_bookings` < `max_bookings`)
600 ORDER BY `date` ASC";
601
602 $query = $this->wpdb->prepare($sql, $tripId, $startDate, $endDate);
603 return $this->wpdb->get_results($query) ?: [];
604 }
605
606 /**
607 * Update current bookings count for a specific date
608 *
609 * @param int $id Specific date record ID
610 * @param int $bookingCount New booking count
611 * @return bool Success status
612 */
613 public function updateBookingCount(int $id, int $bookingCount): bool
614 {
615 $table = esc_sql($this->table);
616
617 $sql = "UPDATE `{$table}`
618 SET `current_bookings` = %d, `updated_at` = NOW()
619 WHERE `id` = %d";
620
621 $query = $this->wpdb->prepare($sql, $bookingCount, $id);
622 return (bool) $this->wpdb->query($query);
623 }
624
625 /**
626 * Increment booking count for a specific date
627 *
628 * @param int $id Specific date record ID
629 * @param int $increment Number to increment by (default: 1)
630 * @return bool Success status
631 */
632 public function incrementBookingCount(int $id, int $increment = 1): bool
633 {
634 $table = esc_sql($this->table);
635
636 $sql = "UPDATE `{$table}`
637 SET `current_bookings` = `current_bookings` + %d, `updated_at` = NOW()
638 WHERE `id` = %d";
639
640 $query = $this->wpdb->prepare($sql, $increment, $id);
641 return (bool) $this->wpdb->query($query);
642 }
643
644 /**
645 * Decrement booking count for a specific date
646 *
647 * @param int $id Specific date record ID
648 * @param int $decrement Number to decrement by (default: 1)
649 * @return bool Success status
650 */
651 public function decrementBookingCount(int $id, int $decrement = 1): bool
652 {
653 $table = esc_sql($this->table);
654
655 $sql = "UPDATE `{$table}`
656 SET `current_bookings` = GREATEST(0, `current_bookings` - %d), `updated_at` = NOW()
657 WHERE `id` = %d";
658
659 $query = $this->wpdb->prepare($sql, $decrement, $id);
660 return (bool) $this->wpdb->query($query);
661 }
662 }
663
664