PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / DepartureRepository.php

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

630 lines 22.1 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\Departure;
8 use Yatra\Database\Tables\DeparturesTable;
9
10 /**
11 * Departure Repository
12 * Handles database operations for trip departures
13 *
14 * Table: wp_yatra_trip_departures
15 *
16 * Fields:
17 * - id (primary key)
18 * - trip_id
19 * - date (YYYY-MM-DD)
20 * - time (HH:MM:SS, nullable)
21 * - max_capacity
22 * - booked_count
23 * - status (upcoming|full|past|cancelled)
24 * - source (manual|recurring_generated)
25 * - price_override (nullable)
26 * - price_by_traveler_type (JSON)
27 * - notes (nullable)
28 * - created_at
29 * - updated_at
30 */
31 class DepartureRepository extends BaseRepository
32 {
33 /**
34 * Get table name
35 */
36 protected function getTableName(): string
37 {
38 return DeparturesTable::getTableName();
39 }
40
41 /**
42 * Find by ID
43 */
44 public function find(int $id, bool $includeDeleted = false): ?\stdClass
45 {
46 $result = parent::find($id, $includeDeleted);
47 return $result ? (object) Departure::fromArray((array) $result)->toArray() : null;
48 }
49
50 /**
51 * Find by ID and return Departure model
52 */
53 public function findModel(int $id): ?Departure
54 {
55 $result = parent::find($id);
56 return $result ? Departure::fromArray((array) $result) : null;
57 }
58
59 /**
60 * Find a departure by trip, start date (or date), and optional time
61 */
62 public function findByTripIdAndStartDate(int $tripId, string $date, ?string $time = null): ?Departure
63 {
64 $table = esc_sql($this->table);
65 $where = ['trip_id = %d'];
66 $params = [$tripId];
67
68 // prefer start_date if exists, else fall back to date column
69 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
70 $hasStartDate = in_array('start_date', $columns, true);
71
72 if ($hasStartDate) {
73 $where[] = '((start_date IS NOT NULL AND start_date = %s) OR (start_date IS NULL AND date = %s))';
74 $params[] = $date;
75 $params[] = $date;
76 } else {
77 $where[] = 'date = %s';
78 $params[] = $date;
79 }
80
81 if ($time !== null && $time !== '') {
82 $where[] = 'time = %s';
83 $params[] = $time;
84 }
85
86 $query = "SELECT * FROM {$table} WHERE " . implode(' AND ', $where) . " LIMIT 1";
87 $row = $this->wpdb->get_row($this->wpdb->prepare($query, ...$params), ARRAY_A);
88
89 return $row ? Departure::fromArray($row) : null;
90 }
91
92 /**
93 * Find all departures across all trips
94 *
95 * @param array $filters Filters: status, date_from, date_to, source
96 * @return array Array of Departure models
97 */
98 public function findAll(array $filters = []): array
99 {
100
101 $table = esc_sql($this->table);
102 $where = ['1=1']; // Always true for base condition
103 $params = [];
104
105 // Status filter
106 if (!empty($filters['status']) && $filters['status'] !== 'all') {
107 $where[] = 'status = %s';
108 $params[] = $filters['status'];
109 }
110
111 // Date range filter - simple approach
112 if (isset($filters['date_from']) && is_string($filters['date_from']) && trim($filters['date_from']) !== '') {
113 $dateFrom = trim($filters['date_from']);
114 // Validate date format AND that it's a real date
115 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) && strtotime($dateFrom) !== false) {
116 $where[] = 'date >= %s';
117 $params[] = $dateFrom;
118 }
119 }
120
121 if (isset($filters['date_to']) && is_string($filters['date_to']) && trim($filters['date_to']) !== '') {
122 $dateTo = trim($filters['date_to']);
123 // Validate date format AND that it's a real date
124 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo) && strtotime($dateTo) !== false) {
125 $where[] = 'date <= %s';
126 $params[] = $dateTo;
127 }
128 }
129
130 // Source filter
131 if (!empty($filters['source']) && $filters['source'] !== 'all') {
132 $where[] = 'source = %s';
133 $params[] = $filters['source'];
134 }
135
136 // Past/upcoming filter
137 if (isset($filters['include_past'])) {
138 if (!$filters['include_past']) {
139 $where[] = 'date >= CURDATE()';
140 }
141 }
142
143 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
144 $query .= " ORDER BY date ASC, time ASC";
145
146 if (!empty($filters['per_page'])) {
147 $perPage = (int) $filters['per_page'];
148 $page = max(1, (int) ($filters['page'] ?? 1));
149 $offset = ($page - 1) * $perPage;
150 $query .= " LIMIT %d OFFSET %d";
151 $params[] = $perPage;
152 $params[] = $offset;
153 }
154
155 // Debug output
156 $prepared_query = $this->wpdb->prepare($query, ...$params);
157 $results = $this->wpdb->get_results($prepared_query, ARRAY_A);
158
159 if ($this->wpdb->last_error) {
160 }
161
162 return array_map(function ($row) {
163 return Departure::fromArray($row);
164 }, $results ?: []);
165 }
166
167 /**
168 * Find departures by trip ID
169 *
170 * @param int $tripId Trip ID
171 * @param array $filters Filters: status, date_from, date_to, source
172 * @return array Array of Departure models
173 */
174 public function findByTripId(int $tripId, array $filters = []): array
175 {
176
177 $table = esc_sql($this->table);
178 $where = ['trip_id = %d'];
179 $params = [$tripId];
180
181 // Status filter
182 if (!empty($filters['status']) && $filters['status'] !== 'all') {
183 $where[] = 'status = %s';
184 $params[] = $filters['status'];
185 }
186
187 // Date range filter - check both start_date and date columns
188 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
189 $hasStartDate = in_array('start_date', $columns, true);
190
191 if (!empty($filters['date_from']) && trim($filters['date_from']) !== '') {
192 if ($hasStartDate) {
193 // NOTE: start_date is a DATE column in some installs; comparing to "" can trigger
194 // "Incorrect DATE value: ''" under strict SQL modes. Treat "0000-00-00" as empty.
195 $where[] = '((start_date IS NOT NULL AND start_date <> %s AND start_date >= %s) OR ((start_date IS NULL OR start_date = %s) AND date >= %s))';
196 $params[] = '0000-00-00';
197 $params[] = $filters['date_from'];
198 $params[] = '0000-00-00';
199 $params[] = $filters['date_from'];
200 } else {
201 $where[] = 'date >= %s';
202 $params[] = $filters['date_from'];
203 }
204 }
205
206 if (!empty($filters['date_to']) && trim($filters['date_to']) !== '') {
207 if ($hasStartDate) {
208 // See note above re strict DATE comparisons and empty string.
209 $where[] = '((start_date IS NOT NULL AND start_date <> %s AND start_date <= %s) OR ((start_date IS NULL OR start_date = %s) AND date <= %s))';
210 $params[] = '0000-00-00';
211 $params[] = $filters['date_to'];
212 $params[] = '0000-00-00';
213 $params[] = $filters['date_to'];
214 } else {
215 $where[] = 'date <= %s';
216 $params[] = $filters['date_to'];
217 }
218 }
219
220 // Source filter
221 if (!empty($filters['source']) && $filters['source'] !== 'all') {
222 $where[] = 'source = %s';
223 $params[] = $filters['source'];
224 }
225
226 // Past/upcoming filter
227 if (isset($filters['include_past'])) {
228 if (!$filters['include_past']) {
229 $where[] = 'date >= CURDATE()';
230 }
231 }
232
233 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
234 $query .= " ORDER BY date ASC, time ASC";
235
236 if (!empty($filters['per_page'])) {
237 $perPage = (int) $filters['per_page'];
238 $page = max(1, (int) ($filters['page'] ?? 1));
239 $offset = ($page - 1) * $perPage;
240 $query .= " LIMIT %d OFFSET %d";
241 $params[] = $perPage;
242 $params[] = $offset;
243 }
244
245 $results = $this->wpdb->get_results(
246 $this->wpdb->prepare($query, ...$params),
247 ARRAY_A
248 );
249
250 return array_map(function ($row) {
251 return Departure::fromArray($row);
252 }, $results ?: []);
253 }
254
255 /**
256 * Find past departures by trip ID
257 */
258 public function findPastByTripId(int $tripId, array $filters = []): array
259 {
260 $filters['status'] = 'past';
261 $filters['include_past'] = true;
262 return $this->findByTripId($tripId, $filters);
263 }
264
265 /**
266 * Find upcoming departures by trip ID
267 */
268 public function findUpcomingByTripId(int $tripId, array $filters = []): array
269 {
270 $filters['include_past'] = false;
271 return $this->findByTripId($tripId, $filters);
272 }
273
274 /**
275 * Find departure by trip ID and date (backward compatibility - uses start_date)
276 */
277 public function findByTripIdAndDate(int $tripId, string $date, ?string $time = null): ?Departure
278 {
279 return $this->findByTripIdAndStartDate($tripId, $date, $time);
280 }
281
282 /**
283 * Count departures by trip ID
284 */
285 public function countByTripId(int $tripId, array $filters = []): int
286 {
287 $table = esc_sql($this->table);
288 $where = ['trip_id = %d'];
289 $params = [$tripId];
290
291 if (!empty($filters['status']) && $filters['status'] !== 'all') {
292 $where[] = 'status = %s';
293 $params[] = $filters['status'];
294 }
295
296 if (!empty($filters['date_from'])) {
297 $where[] = 'date >= %s';
298 $params[] = $filters['date_from'];
299 }
300
301 if (!empty($filters['date_to'])) {
302 $where[] = 'date <= %s';
303 $params[] = $filters['date_to'];
304 }
305
306 if (!empty($filters['source']) && $filters['source'] !== 'all') {
307 $where[] = 'source = %s';
308 $params[] = $filters['source'];
309 }
310
311 if (isset($filters['include_past']) && !$filters['include_past']) {
312 $where[] = 'date >= CURDATE()';
313 }
314
315 $query = "SELECT COUNT(*) FROM `{$table}` WHERE " . implode(' AND ', $where);
316
317 return (int) $this->wpdb->get_var($this->wpdb->prepare($query, ...$params));
318 }
319
320 /**
321 * Create a departure
322 */
323 public function create(array $data): int
324 {
325 $table = esc_sql($this->table);
326
327 // Check which columns exist in the table
328 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
329 $hasStartDate = in_array('start_date', $columns, true);
330 $hasEndDate = in_array('end_date', $columns, true);
331
332 // Handle start_date and end_date - support both old 'date' and new format
333 $startDate = !empty($data['start_date']) ? $data['start_date'] : ($data['date'] ?? '');
334 $endDate = $data['end_date'] ?? '';
335
336 $insertData = [
337 'trip_id' => (int) ($data['trip_id'] ?? 0),
338 'date' => sanitize_text_field($startDate), // Always include for backward compatibility
339 'time' => !empty($data['time']) ? sanitize_text_field($data['time']) : null,
340 'max_capacity' => (int) ($data['max_capacity'] ?? 0),
341 'booked_count' => (int) ($data['booked_count'] ?? 0),
342 'status' => sanitize_text_field($data['status'] ?? 'upcoming'),
343 'source' => sanitize_text_field($data['source'] ?? 'booking_created'),
344 'price_override' => !empty($data['price_override']) ? (float) $data['price_override'] : null,
345 'notes' => !empty($data['notes']) ? sanitize_textarea_field($data['notes']) : null,
346 'created_at' => current_time('mysql'),
347 'updated_at' => current_time('mysql'),
348 ];
349
350 // Only add start_date and end_date if columns exist
351 if ($hasStartDate) {
352 $insertData['start_date'] = sanitize_text_field($startDate);
353 }
354 if ($hasEndDate && !empty($endDate)) {
355 $insertData['end_date'] = sanitize_text_field($endDate);
356 }
357
358 // Only add total_revenue if column exists
359 $hasTotalRevenue = in_array('total_revenue', $columns, true);
360 if ($hasTotalRevenue) {
361 $insertData['total_revenue'] = !empty($data['total_revenue']) ? (float) $data['total_revenue'] : 0.00;
362 }
363
364 // Handle price_by_traveler_type as JSON
365 if (!empty($data['price_by_traveler_type'])) {
366 $insertData['price_by_traveler_type'] = is_array($data['price_by_traveler_type'])
367 ? wp_json_encode($data['price_by_traveler_type'])
368 : $data['price_by_traveler_type'];
369 } else {
370 $insertData['price_by_traveler_type'] = null;
371 }
372
373 // Calculate status if not provided
374 if (empty($data['status'])) {
375 $departure = Departure::fromArray($insertData);
376 $insertData['status'] = $departure->calculateStatus();
377 }
378
379 // Build format array dynamically based on what we're inserting
380 $formats = [];
381 foreach ($insertData as $key => $value) {
382 if (in_array($key, ['trip_id', 'max_capacity', 'booked_count'], true)) {
383 $formats[] = '%d';
384 } elseif (in_array($key, ['price_override'], true)) {
385 $formats[] = '%f';
386 } else {
387 $formats[] = '%s';
388 }
389 }
390
391 $this->wpdb->insert($table, $insertData, $formats);
392
393 return $this->wpdb->insert_id;
394 }
395
396 /**
397 * Update a departure
398 */
399 public function update(int $id, array $data): bool
400 {
401 $table = esc_sql($this->table);
402
403 // Check which columns exist
404 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
405 $hasStartDate = in_array('start_date', $columns, true);
406 $hasEndDate = in_array('end_date', $columns, true);
407
408 $updateData = [];
409
410 if (isset($data['trip_id'])) $updateData['trip_id'] = (int) $data['trip_id'];
411
412 // Handle date fields - support both old 'date' and new 'start_date'/'end_date'
413 if (isset($data['start_date'])) {
414 if ($hasStartDate) {
415 $updateData['start_date'] = sanitize_text_field($data['start_date']);
416 }
417 $updateData['date'] = sanitize_text_field($data['start_date']); // Always update date for backward compatibility
418 } elseif (isset($data['date'])) {
419 $updateData['date'] = sanitize_text_field($data['date']);
420 if ($hasStartDate) {
421 $updateData['start_date'] = $updateData['date']; // Sync start_date
422 }
423 }
424
425 if (isset($data['end_date']) && $hasEndDate) {
426 $updateData['end_date'] = sanitize_text_field($data['end_date']);
427 }
428 if (isset($data['time'])) $updateData['time'] = !empty($data['time']) ? sanitize_text_field($data['time']) : null;
429 if (isset($data['max_capacity'])) $updateData['max_capacity'] = (int) $data['max_capacity'];
430 if (isset($data['booked_count'])) $updateData['booked_count'] = (int) $data['booked_count'];
431 if (isset($data['status'])) $updateData['status'] = sanitize_text_field($data['status']);
432 if (isset($data['source'])) $updateData['source'] = sanitize_text_field($data['source']);
433 if (isset($data['price_override'])) $updateData['price_override'] = !empty($data['price_override']) ? (float) $data['price_override'] : null;
434
435 // Only update total_revenue if column exists
436 $hasTotalRevenue = in_array('total_revenue', $columns, true);
437 if (isset($data['total_revenue']) && $hasTotalRevenue) {
438 $updateData['total_revenue'] = !empty($data['total_revenue']) ? (float) $data['total_revenue'] : 0.00;
439 }
440
441 if (isset($data['notes'])) $updateData['notes'] = !empty($data['notes']) ? sanitize_textarea_field($data['notes']) : null;
442
443 if (isset($data['price_by_traveler_type'])) {
444 $updateData['price_by_traveler_type'] = is_array($data['price_by_traveler_type'])
445 ? wp_json_encode($data['price_by_traveler_type'])
446 : $data['price_by_traveler_type'];
447 }
448
449 $updateData['updated_at'] = current_time('mysql');
450
451 // Recalculate status if date or capacity changed
452 if (isset($updateData['start_date']) || isset($updateData['end_date']) || isset($updateData['date']) ||
453 isset($updateData['max_capacity']) || isset($updateData['booked_count'])) {
454 // Get current departure to merge with updates
455 $current = $this->findModel($id);
456 if ($current) {
457 $merged = array_merge($current->toArray(), $updateData);
458 $departure = Departure::fromArray($merged);
459 $updateData['status'] = $departure->calculateStatus();
460 }
461 }
462
463 if (empty($updateData)) {
464 return false;
465 }
466
467 $formats = [];
468 foreach ($updateData as $value) {
469 if (is_int($value)) {
470 $formats[] = '%d';
471 } elseif (is_float($value)) {
472 $formats[] = '%f';
473 } else {
474 $formats[] = '%s';
475 }
476 }
477
478 return (bool) $this->wpdb->update(
479 $table,
480 $updateData,
481 ['id' => $id],
482 $formats,
483 ['%d']
484 );
485 }
486
487 /**
488 * Increment booked count
489 */
490 public function incrementBookedCount(int $id, int $amount = 1): bool
491 {
492 $table = esc_sql($this->table);
493
494 $this->wpdb->query($this->wpdb->prepare(
495 "UPDATE `{$table}`
496 SET booked_count = booked_count + %d,
497 updated_at = %s
498 WHERE id = %d",
499 $amount,
500 current_time('mysql'),
501 $id
502 ));
503
504 // Recalculate status
505 $departure = $this->findModel($id);
506 if ($departure) {
507 $this->update($id, ['status' => $departure->calculateStatus()]);
508 }
509
510 return true;
511 }
512
513 /**
514 * Decrement booked count
515 */
516 public function decrementBookedCount(int $id, int $amount = 1): bool
517 {
518 $table = esc_sql($this->table);
519
520 $this->wpdb->query($this->wpdb->prepare(
521 "UPDATE `{$table}`
522 SET booked_count = GREATEST(0, booked_count - %d),
523 updated_at = %s
524 WHERE id = %d",
525 $amount,
526 current_time('mysql'),
527 $id
528 ));
529
530 // Recalculate status
531 $departure = $this->findModel($id);
532 if ($departure) {
533 $this->update($id, ['status' => $departure->calculateStatus()]);
534 }
535
536 return true;
537 }
538
539 /**
540 * Delete a departure
541 * Only allowed if source is recurring_generated and booked_count is 0
542 */
543 public function delete(int $id): bool
544 {
545 $departure = $this->findModel($id);
546
547 if (!$departure) {
548 return false;
549 }
550
551 // Only allow deletion of recurring_generated departures with no bookings
552 if ($departure->source === 'recurring_generated' && $departure->booked_count === 0) {
553 $table = esc_sql($this->table);
554 return (bool) $this->wpdb->delete($table, ['id' => $id], ['%d']);
555 }
556
557 // Manual departures or departures with bookings cannot be deleted
558 return false;
559 }
560
561 /**
562 * Recalculate status for all departures (for cron job)
563 */
564 public function recalculateAllStatuses(): int
565 {
566 $table = esc_sql($this->table);
567 $today = date('Y-m-d');
568
569 // Update past departures - use end_date if available, otherwise start_date or date
570 $this->wpdb->query($this->wpdb->prepare(
571 "UPDATE `{$table}`
572 SET status = 'past', updated_at = %s
573 WHERE (
574 (end_date IS NOT NULL AND end_date != '' AND end_date < %s) OR
575 (end_date IS NULL OR end_date = '') AND (
576 (start_date IS NOT NULL AND start_date != '' AND start_date < %s) OR
577 (start_date IS NULL OR start_date = '') AND date < %s
578 )
579 )
580 AND status != 'cancelled'",
581 current_time('mysql'),
582 $today,
583 $today,
584 $today
585 ));
586
587 // Update full departures - check future dates only
588 $this->wpdb->query(
589 "UPDATE `{$table}`
590 SET status = 'full', updated_at = NOW()
591 WHERE booked_count >= max_capacity
592 AND max_capacity > 0
593 AND (
594 (end_date IS NOT NULL AND end_date != '' AND end_date >= CURDATE()) OR
595 (end_date IS NULL OR end_date = '') AND (
596 (start_date IS NOT NULL AND start_date != '' AND start_date >= CURDATE()) OR
597 (start_date IS NULL OR start_date = '') AND date >= CURDATE()
598 )
599 )
600 AND status NOT IN ('cancelled', 'past')"
601 );
602
603 // Update upcoming departures - check future dates only
604 $this->wpdb->query(
605 "UPDATE `{$table}`
606 SET status = 'upcoming', updated_at = NOW()
607 WHERE (
608 (end_date IS NOT NULL AND end_date != '' AND end_date >= CURDATE()) OR
609 (end_date IS NULL OR end_date = '') AND (
610 (start_date IS NOT NULL AND start_date != '' AND start_date >= CURDATE()) OR
611 (start_date IS NULL OR start_date = '') AND date >= CURDATE()
612 )
613 )
614 AND booked_count < max_capacity
615 AND status NOT IN ('cancelled', 'past', 'full')"
616 );
617
618 return $this->wpdb->rows_affected;
619 }
620
621 /**
622 * Check if table supports soft delete
623 */
624 protected function hasSoftDelete(): bool
625 {
626 return false;
627 }
628 }
629
630