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

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