PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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 / YatraAvailabilityRepository.php

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

466 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Repositories;
4
5 use Yatra\Database\Tables\YatraAvailabilityTable;
6
7 /**
8 * Yatra Availability Repository
9 *
10 * Handles database operations for traditional Yatra trip availability dates.
11 * Provides methods for managing departure dates, pricing, capacity, and bookings.
12 *
13 * @package Yatra\Repositories
14 * @since 2.0.0
15 */
16 class YatraAvailabilityRepository extends BaseRepository
17 {
18 /**
19 * Table name
20 */
21 protected string $table;
22
23 /**
24 * Constructor
25 */
26 public function __construct()
27 {
28 global $wpdb;
29 $this->table = YatraAvailabilityTable::getTableName();
30 parent::__construct();
31 }
32
33 /**
34 * Find availability by trip ID and departure date
35 *
36 * @param int $tripId Trip ID
37 * @param string $departureDate Departure date (YYYY-MM-DD)
38 * @return object|null Availability object or null
39 */
40 public function findByTripIdAndDate(int $tripId, string $departureDate): ?object
41 {
42 $table = esc_sql($this->table);
43 $tripId = (int) $tripId;
44 $departureDate = esc_sql($departureDate);
45
46 $query = $this->wpdb->prepare(
47 "SELECT * FROM `{$table}`
48 WHERE trip_id = %d AND departure_date = %s
49 LIMIT 1",
50 $tripId,
51 $departureDate
52 );
53
54 return $this->wpdb->get_row($query) ?: null;
55 }
56
57 /**
58 * Get availability dates for a trip within a date range
59 *
60 * @param int $tripId Trip ID
61 * @param string $startDate Start date (YYYY-MM-DD)
62 * @param string $endDate End date (YYYY-MM-DD)
63 * @param array $args Additional arguments
64 * @return array Array of availability objects
65 */
66 public function getDatesForTrip(int $tripId, string $startDate, string $endDate, array $args = []): array
67 {
68 $table = esc_sql($this->table);
69 $tripId = (int) $tripId;
70 $startDate = esc_sql($startDate);
71 $endDate = esc_sql($endDate);
72
73 $where = ["trip_id = {$tripId}", "departure_date BETWEEN '{$startDate}' AND '{$endDate}'"];
74
75 if (!empty($args['status'])) {
76 $where[] = "status = '" . esc_sql($args['status']) . "'";
77 }
78
79 if (!empty($args['available_only'])) {
80 $where[] = "seats_available > 0";
81 $where[] = "status = 'available'";
82 }
83
84 if (!empty($args['not_blocked'])) {
85 $where[] = "is_blocked = 0";
86 }
87
88 $whereClause = implode(' AND ', $where);
89
90 $order = !empty($args['order']) ? esc_sql($args['order']) : 'departure_date ASC';
91 $limit = !empty($args['limit']) ? (int) $args['limit'] : '';
92
93 $sql = "SELECT * FROM `{$table}`
94 WHERE {$whereClause}
95 ORDER BY {$order}";
96
97 if ($limit) {
98 $sql .= " LIMIT {$limit}";
99 }
100
101 return $this->wpdb->get_results($sql) ?: [];
102 }
103
104 /**
105 * Get available dates for a trip (with seats available)
106 *
107 * @param int $tripId Trip ID
108 * @param string $startDate Start date (YYYY-MM-DD)
109 * @param string $endDate End date (YYYY-MM-DD)
110 * @param int $minSeats Minimum seats required (default: 1)
111 * @return array Array of available dates
112 */
113 public function getAvailableDates(int $tripId, string $startDate, string $endDate, int $minSeats = 1): array
114 {
115 $table = esc_sql($this->table);
116 $tripId = (int) $tripId;
117 $startDate = esc_sql($startDate);
118 $endDate = esc_sql($endDate);
119 $minSeats = (int) $minSeats;
120
121 $sql = "SELECT * FROM `{$table}`
122 WHERE trip_id = {$tripId}
123 AND departure_date BETWEEN '{$startDate}' AND '{$endDate}'
124 AND seats_available >= {$minSeats}
125 AND status = 'available'
126 AND is_blocked = 0
127 AND (cutoff_date IS NULL OR cutoff_date >= CURDATE())
128 ORDER BY departure_date ASC";
129
130 return $this->wpdb->get_results($sql) ?: [];
131 }
132
133 /**
134 * Check if a specific date is available for booking
135 *
136 * @param int $tripId Trip ID
137 * @param string $departureDate Departure date (YYYY-MM-DD)
138 * @param int $requiredSeats Seats required (default: 1)
139 * @return bool True if available, false otherwise
140 */
141 public function isDateAvailable(int $tripId, string $departureDate, int $requiredSeats = 1): bool
142 {
143 $table = esc_sql($this->table);
144 $tripId = (int) $tripId;
145 $departureDate = esc_sql($departureDate);
146 $requiredSeats = (int) $requiredSeats;
147
148 $sql = "SELECT COUNT(*) as count
149 FROM `{$table}`
150 WHERE trip_id = {$tripId}
151 AND departure_date = '{$departureDate}'
152 AND seats_available >= {$requiredSeats}
153 AND status = 'available'
154 AND is_blocked = 0
155 AND (cutoff_date IS NULL OR cutoff_date >= CURDATE())";
156
157 $result = $this->wpdb->get_var($sql);
158 return (int) $result > 0;
159 }
160
161 /**
162 * Update seat counts for a departure date
163 *
164 * @param int $tripId Trip ID
165 * @param string $departureDate Departure date (YYYY-MM-DD)
166 * @param int $totalSeats Total seats
167 * @param int $reservedSeats Reserved seats
168 * @return bool Success status
169 */
170 public function updateSeatCounts(int $tripId, string $departureDate, int $totalSeats, int $reservedSeats): bool
171 {
172 $table = esc_sql($this->table);
173 $tripId = (int) $tripId;
174 $departureDate = esc_sql($departureDate);
175 $totalSeats = (int) $totalSeats;
176 $reservedSeats = (int) $reservedSeats;
177 $availableSeats = max(0, $totalSeats - $reservedSeats);
178
179 $sql = "UPDATE `{$table}`
180 SET seats_total = {$totalSeats},
181 seats_reserved = {$reservedSeats},
182 seats_available = {$availableSeats},
183 updated_at = CURRENT_TIMESTAMP
184 WHERE trip_id = {$tripId} AND departure_date = '{$departureDate}'";
185
186 return $this->wpdb->query($sql) !== false;
187 }
188
189 /**
190 * Reserve seats for a departure date
191 *
192 * @param int $tripId Trip ID
193 * @param string $departureDate Departure date (YYYY-MM-DD)
194 * @param int $seatsToReserve Number of seats to reserve
195 * @return bool Success status
196 */
197 public function reserveSeats(int $tripId, string $departureDate, int $seatsToReserve): bool
198 {
199 $table = esc_sql($this->table);
200 $tripId = (int) $tripId;
201 $departureDate = esc_sql($departureDate);
202 $seatsToReserve = (int) $seatsToReserve;
203
204 if ($seatsToReserve <= 0) return false;
205
206 // Early-exit when there's clearly not enough capacity. This is
207 // a "fail fast" optimisation — the atomic UPDATE below is the
208 // actual authority (the AND seats_available >= guard prevents
209 // overbooking even if this pre-check raced with a concurrent
210 // reservation), but skipping the UPDATE round-trip when the
211 // answer is already obvious is cheap.
212 $available = $this->getAvailableSeats($tripId, $departureDate);
213 if ($available < $seatsToReserve) {
214 return false;
215 }
216
217 $sql = "UPDATE `{$table}`
218 SET seats_reserved = seats_reserved + {$seatsToReserve},
219 seats_available = seats_available - {$seatsToReserve},
220 updated_at = CURRENT_TIMESTAMP
221 WHERE trip_id = {$tripId}
222 AND departure_date = '{$departureDate}'
223 AND seats_available >= {$seatsToReserve}";
224
225 // CRITICAL: anti-overbooking authority. `$wpdb->query()`
226 // returns the number of rows affected for UPDATE statements,
227 // or `false` on error. We MUST distinguish:
228 //
229 // - false → SQL error (return false; caller treats
230 // as failed reservation).
231 // - 0 rows → another writer beat us to the seats; the
232 // `AND seats_available >= …` guard rejected
233 // the write. Return false — this is the
234 // overbooking-prevention path.
235 // - 1+ rows → reservation succeeded atomically. The
236 // decrement-and-guard happened in a single
237 // DB statement so no race is possible.
238 //
239 // Previous behaviour was `return $wpdb->query($sql) !== false`
240 // which incorrectly returned TRUE on the 0-rows case — meaning
241 // callers thought they had reserved seats when in fact the DB
242 // had refused the write. With concurrent OTA + local bookings,
243 // that resulted in real overbooking even though the SQL was
244 // safe.
245 $rows = $this->wpdb->query($sql);
246 if ($rows === false) return false;
247 return (int) $rows > 0;
248 }
249
250 /**
251 * Release reserved seats for a departure date
252 *
253 * @param int $tripId Trip ID
254 * @param string $departureDate Departure date (YYYY-MM-DD)
255 * @param int $seatsToRelease Number of seats to release
256 * @return bool Success status
257 */
258 public function releaseSeats(int $tripId, string $departureDate, int $seatsToRelease): bool
259 {
260 $table = esc_sql($this->table);
261 $tripId = (int) $tripId;
262 $departureDate = esc_sql($departureDate);
263 $seatsToRelease = (int) $seatsToRelease;
264
265 $sql = "UPDATE `{$table}`
266 SET seats_reserved = GREATEST(0, seats_reserved - {$seatsToRelease}),
267 seats_available = LEAST(seats_total, seats_available + {$seatsToRelease}),
268 updated_at = CURRENT_TIMESTAMP
269 WHERE trip_id = {$tripId} AND departure_date = '{$departureDate}'";
270
271 return $this->wpdb->query($sql) !== false;
272 }
273
274 /**
275 * Get available seats count for a departure date
276 *
277 * @param int $tripId Trip ID
278 * @param string $departureDate Departure date (YYYY-MM-DD)
279 * @return int Number of available seats
280 */
281 public function getAvailableSeats(int $tripId, string $departureDate): int
282 {
283 $table = esc_sql($this->table);
284 $tripId = (int) $tripId;
285 $departureDate = esc_sql($departureDate);
286
287 $sql = "SELECT seats_available
288 FROM `{$table}`
289 WHERE trip_id = {$tripId} AND departure_date = '{$departureDate}'
290 AND status = 'available' AND is_blocked = 0
291 LIMIT 1";
292
293 $result = $this->wpdb->get_var($sql);
294 return (int) $result ?: 0;
295 }
296
297 /**
298 * Block/unblock a departure date
299 *
300 * @param int $tripId Trip ID
301 * @param string $departureDate Departure date (YYYY-MM-DD)
302 * @param bool $blocked Whether to block
303 * @param string|null $reason Block reason
304 * @return bool Success status
305 */
306 public function blockDate(int $tripId, string $departureDate, bool $blocked = true, ?string $reason = null): bool
307 {
308 $table = esc_sql($this->table);
309 $tripId = (int) $tripId;
310 $departureDate = esc_sql($departureDate);
311 $blockedInt = $blocked ? 1 : 0;
312 $reason = $reason ? "'" . esc_sql($reason) . "'" : 'NULL';
313
314 $sql = "UPDATE `{$table}`
315 SET is_blocked = {$blockedInt},
316 block_reason = {$reason},
317 updated_at = CURRENT_TIMESTAMP
318 WHERE trip_id = {$tripId} AND departure_date = '{$departureDate}'";
319
320 return $this->wpdb->query($sql) !== false;
321 }
322
323 /**
324 * Update status for a departure date
325 *
326 * @param int $tripId Trip ID
327 * @param string $departureDate Departure date (YYYY-MM-DD)
328 * @param string $status New status
329 * @return bool Success status
330 */
331 public function updateStatus(int $tripId, string $departureDate, string $status): bool
332 {
333 $table = esc_sql($this->table);
334 $tripId = (int) $tripId;
335 $departureDate = esc_sql($departureDate);
336 $status = esc_sql($status);
337
338 $sql = "UPDATE `{$table}`
339 SET status = '{$status}',
340 updated_at = CURRENT_TIMESTAMP
341 WHERE trip_id = {$tripId} AND departure_date = '{$departureDate}'";
342
343 return $this->wpdb->query($sql) !== false;
344 }
345
346 /**
347 * Get availability statistics for a trip
348 *
349 * @param int $tripId Trip ID
350 * @param string $startDate Start date (YYYY-MM-DD)
351 * @param string $endDate End date (YYYY-MM-DD)
352 * @return array Statistics array
353 */
354 public function getTripStatistics(int $tripId, string $startDate, string $endDate): array
355 {
356 $table = esc_sql($this->table);
357 $tripId = (int) $tripId;
358 $startDate = esc_sql($startDate);
359 $endDate = esc_sql($endDate);
360
361 $sql = "SELECT
362 COUNT(*) as total_dates,
363 SUM(CASE WHEN status = 'available' AND is_blocked = 0 THEN 1 ELSE 0 END) as available_dates,
364 SUM(CASE WHEN seats_available > 0 THEN 1 ELSE 0 END) as dates_with_seats,
365 SUM(seats_total) as total_capacity,
366 SUM(seats_available) as total_available,
367 SUM(seats_reserved) as total_reserved,
368 AVG(seats_available) as avg_available_seats,
369 MIN(departure_date) as first_date,
370 MAX(departure_date) as last_date
371 FROM `{$table}`
372 WHERE trip_id = {$tripId}
373 AND departure_date BETWEEN '{$startDate}' AND '{$endDate}'";
374
375 $result = $this->wpdb->get_row($sql);
376 return $result ? (array) $result : [];
377 }
378
379 /**
380 * Delete availability dates for a trip within a date range
381 *
382 * @param int $tripId Trip ID
383 * @param string $startDate Start date (YYYY-MM-DD)
384 * @param string $endDate End date (YYYY-MM-DD)
385 * @return int Number of deleted records
386 */
387 public function deleteDateRange(int $tripId, string $startDate, string $endDate): int
388 {
389 $table = esc_sql($this->table);
390 $tripId = (int) $tripId;
391 $startDate = esc_sql($startDate);
392 $endDate = esc_sql($endDate);
393
394 $sql = "DELETE FROM `{$table}`
395 WHERE trip_id = {$tripId}
396 AND departure_date BETWEEN '{$startDate}' AND '{$endDate}'";
397
398 return $this->wpdb->query($sql) ?: 0;
399 }
400
401 /**
402 * Get upcoming departures that need alerts
403 *
404 * @param int $daysAhead Number of days ahead to check
405 * @param int $alertThreshold Alert threshold for seats
406 * @return array Array of departures needing alerts
407 */
408 public function getDeparturesNeedingAlerts(int $daysAhead = 7, int $alertThreshold = 3): array
409 {
410 $table = esc_sql($this->table);
411 $daysAhead = (int) $daysAhead;
412 $alertThreshold = (int) $alertThreshold;
413
414 $sql = "SELECT * FROM `{$table}`
415 WHERE departure_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL {$daysAhead} DAY)
416 AND seats_available > 0
417 AND seats_available <= {$alertThreshold}
418 AND status = 'available'
419 AND is_blocked = 0
420 ORDER BY departure_date ASC";
421
422 return $this->wpdb->get_results($sql) ?: [];
423 }
424
425 /**
426 * Get status counts for availability dates
427 *
428 * @param array $args Arguments including trip_id filtering
429 * @return array Status counts
430 */
431 public function getStatusCounts(array $args = []): array
432 {
433 $table = esc_sql($this->table);
434 $where = $this->buildWhereClause($args);
435
436 $sql = "SELECT status, COUNT(*) as count
437 FROM `{$table}`
438 {$where}
439 GROUP BY status";
440
441 $results = $this->wpdb->get_results($sql) ?: [];
442
443 $counts = [
444 'available' => 0,
445 'unavailable' => 0,
446 'limited' => 0,
447 'sold_out' => 0,
448 'cancelled' => 0,
449 'blocked' => 0,
450 'total' => 0
451 ];
452
453 foreach ($results as $row) {
454 $status = $row->status;
455 $count = (int) $row->count;
456
457 if (isset($counts[$status])) {
458 $counts[$status] = $count;
459 }
460 $counts['total'] += $count;
461 }
462
463 return $counts;
464 }
465 }
466