PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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 2.0.1 2.0.10 All 83 releases
yatra / app / Repositories / DepartureRepository.php

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

623 lines 21.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\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 $where[] = '(start_date >= %s OR ((start_date IS NULL OR start_date = "") AND date >= %s))';
194 $params[] = $filters['date_from'];
195 $params[] = $filters['date_from'];
196 } else {
197 $where[] = 'date >= %s';
198 $params[] = $filters['date_from'];
199 }
200 }
201
202 if (!empty($filters['date_to']) && trim($filters['date_to']) !== '') {
203 if ($hasStartDate) {
204 $where[] = '(start_date <= %s OR ((start_date IS NULL OR start_date = "") AND date <= %s))';
205 $params[] = $filters['date_to'];
206 $params[] = $filters['date_to'];
207 } else {
208 $where[] = 'date <= %s';
209 $params[] = $filters['date_to'];
210 }
211 }
212
213 // Source filter
214 if (!empty($filters['source']) && $filters['source'] !== 'all') {
215 $where[] = 'source = %s';
216 $params[] = $filters['source'];
217 }
218
219 // Past/upcoming filter
220 if (isset($filters['include_past'])) {
221 if (!$filters['include_past']) {
222 $where[] = 'date >= CURDATE()';
223 }
224 }
225
226 $query = "SELECT * FROM `{$table}` WHERE " . implode(' AND ', $where);
227 $query .= " ORDER BY date ASC, time ASC";
228
229 if (!empty($filters['per_page'])) {
230 $perPage = (int) $filters['per_page'];
231 $page = max(1, (int) ($filters['page'] ?? 1));
232 $offset = ($page - 1) * $perPage;
233 $query .= " LIMIT %d OFFSET %d";
234 $params[] = $perPage;
235 $params[] = $offset;
236 }
237
238 $results = $this->wpdb->get_results(
239 $this->wpdb->prepare($query, ...$params),
240 ARRAY_A
241 );
242
243 return array_map(function ($row) {
244 return Departure::fromArray($row);
245 }, $results ?: []);
246 }
247
248 /**
249 * Find past departures by trip ID
250 */
251 public function findPastByTripId(int $tripId, array $filters = []): array
252 {
253 $filters['status'] = 'past';
254 $filters['include_past'] = true;
255 return $this->findByTripId($tripId, $filters);
256 }
257
258 /**
259 * Find upcoming departures by trip ID
260 */
261 public function findUpcomingByTripId(int $tripId, array $filters = []): array
262 {
263 $filters['include_past'] = false;
264 return $this->findByTripId($tripId, $filters);
265 }
266
267 /**
268 * Find departure by trip ID and date (backward compatibility - uses start_date)
269 */
270 public function findByTripIdAndDate(int $tripId, string $date, ?string $time = null): ?Departure
271 {
272 return $this->findByTripIdAndStartDate($tripId, $date, $time);
273 }
274
275 /**
276 * Count departures by trip ID
277 */
278 public function countByTripId(int $tripId, array $filters = []): int
279 {
280 $table = esc_sql($this->table);
281 $where = ['trip_id = %d'];
282 $params = [$tripId];
283
284 if (!empty($filters['status']) && $filters['status'] !== 'all') {
285 $where[] = 'status = %s';
286 $params[] = $filters['status'];
287 }
288
289 if (!empty($filters['date_from'])) {
290 $where[] = 'date >= %s';
291 $params[] = $filters['date_from'];
292 }
293
294 if (!empty($filters['date_to'])) {
295 $where[] = 'date <= %s';
296 $params[] = $filters['date_to'];
297 }
298
299 if (!empty($filters['source']) && $filters['source'] !== 'all') {
300 $where[] = 'source = %s';
301 $params[] = $filters['source'];
302 }
303
304 if (isset($filters['include_past']) && !$filters['include_past']) {
305 $where[] = 'date >= CURDATE()';
306 }
307
308 $query = "SELECT COUNT(*) FROM `{$table}` WHERE " . implode(' AND ', $where);
309
310 return (int) $this->wpdb->get_var($this->wpdb->prepare($query, ...$params));
311 }
312
313 /**
314 * Create a departure
315 */
316 public function create(array $data): int
317 {
318 $table = esc_sql($this->table);
319
320 // Check which columns exist in the table
321 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
322 $hasStartDate = in_array('start_date', $columns, true);
323 $hasEndDate = in_array('end_date', $columns, true);
324
325 // Handle start_date and end_date - support both old 'date' and new format
326 $startDate = !empty($data['start_date']) ? $data['start_date'] : ($data['date'] ?? '');
327 $endDate = $data['end_date'] ?? '';
328
329 $insertData = [
330 'trip_id' => (int) ($data['trip_id'] ?? 0),
331 'date' => sanitize_text_field($startDate), // Always include for backward compatibility
332 'time' => !empty($data['time']) ? sanitize_text_field($data['time']) : null,
333 'max_capacity' => (int) ($data['max_capacity'] ?? 0),
334 'booked_count' => (int) ($data['booked_count'] ?? 0),
335 'status' => sanitize_text_field($data['status'] ?? 'upcoming'),
336 'source' => sanitize_text_field($data['source'] ?? 'booking_created'),
337 'price_override' => !empty($data['price_override']) ? (float) $data['price_override'] : null,
338 'notes' => !empty($data['notes']) ? sanitize_textarea_field($data['notes']) : null,
339 'created_at' => current_time('mysql'),
340 'updated_at' => current_time('mysql'),
341 ];
342
343 // Only add start_date and end_date if columns exist
344 if ($hasStartDate) {
345 $insertData['start_date'] = sanitize_text_field($startDate);
346 }
347 if ($hasEndDate && !empty($endDate)) {
348 $insertData['end_date'] = sanitize_text_field($endDate);
349 }
350
351 // Only add total_revenue if column exists
352 $hasTotalRevenue = in_array('total_revenue', $columns, true);
353 if ($hasTotalRevenue) {
354 $insertData['total_revenue'] = !empty($data['total_revenue']) ? (float) $data['total_revenue'] : 0.00;
355 }
356
357 // Handle price_by_traveler_type as JSON
358 if (!empty($data['price_by_traveler_type'])) {
359 $insertData['price_by_traveler_type'] = is_array($data['price_by_traveler_type'])
360 ? wp_json_encode($data['price_by_traveler_type'])
361 : $data['price_by_traveler_type'];
362 } else {
363 $insertData['price_by_traveler_type'] = null;
364 }
365
366 // Calculate status if not provided
367 if (empty($data['status'])) {
368 $departure = Departure::fromArray($insertData);
369 $insertData['status'] = $departure->calculateStatus();
370 }
371
372 // Build format array dynamically based on what we're inserting
373 $formats = [];
374 foreach ($insertData as $key => $value) {
375 if (in_array($key, ['trip_id', 'max_capacity', 'booked_count'], true)) {
376 $formats[] = '%d';
377 } elseif (in_array($key, ['price_override'], true)) {
378 $formats[] = '%f';
379 } else {
380 $formats[] = '%s';
381 }
382 }
383
384 $this->wpdb->insert($table, $insertData, $formats);
385
386 return $this->wpdb->insert_id;
387 }
388
389 /**
390 * Update a departure
391 */
392 public function update(int $id, array $data): bool
393 {
394 $table = esc_sql($this->table);
395
396 // Check which columns exist
397 $columns = $this->wpdb->get_col("DESCRIBE {$table}");
398 $hasStartDate = in_array('start_date', $columns, true);
399 $hasEndDate = in_array('end_date', $columns, true);
400
401 $updateData = [];
402
403 if (isset($data['trip_id'])) $updateData['trip_id'] = (int) $data['trip_id'];
404
405 // Handle date fields - support both old 'date' and new 'start_date'/'end_date'
406 if (isset($data['start_date'])) {
407 if ($hasStartDate) {
408 $updateData['start_date'] = sanitize_text_field($data['start_date']);
409 }
410 $updateData['date'] = sanitize_text_field($data['start_date']); // Always update date for backward compatibility
411 } elseif (isset($data['date'])) {
412 $updateData['date'] = sanitize_text_field($data['date']);
413 if ($hasStartDate) {
414 $updateData['start_date'] = $updateData['date']; // Sync start_date
415 }
416 }
417
418 if (isset($data['end_date']) && $hasEndDate) {
419 $updateData['end_date'] = sanitize_text_field($data['end_date']);
420 }
421 if (isset($data['time'])) $updateData['time'] = !empty($data['time']) ? sanitize_text_field($data['time']) : null;
422 if (isset($data['max_capacity'])) $updateData['max_capacity'] = (int) $data['max_capacity'];
423 if (isset($data['booked_count'])) $updateData['booked_count'] = (int) $data['booked_count'];
424 if (isset($data['status'])) $updateData['status'] = sanitize_text_field($data['status']);
425 if (isset($data['source'])) $updateData['source'] = sanitize_text_field($data['source']);
426 if (isset($data['price_override'])) $updateData['price_override'] = !empty($data['price_override']) ? (float) $data['price_override'] : null;
427
428 // Only update total_revenue if column exists
429 $hasTotalRevenue = in_array('total_revenue', $columns, true);
430 if (isset($data['total_revenue']) && $hasTotalRevenue) {
431 $updateData['total_revenue'] = !empty($data['total_revenue']) ? (float) $data['total_revenue'] : 0.00;
432 }
433
434 if (isset($data['notes'])) $updateData['notes'] = !empty($data['notes']) ? sanitize_textarea_field($data['notes']) : null;
435
436 if (isset($data['price_by_traveler_type'])) {
437 $updateData['price_by_traveler_type'] = is_array($data['price_by_traveler_type'])
438 ? wp_json_encode($data['price_by_traveler_type'])
439 : $data['price_by_traveler_type'];
440 }
441
442 $updateData['updated_at'] = current_time('mysql');
443
444 // Recalculate status if date or capacity changed
445 if (isset($updateData['start_date']) || isset($updateData['end_date']) || isset($updateData['date']) ||
446 isset($updateData['max_capacity']) || isset($updateData['booked_count'])) {
447 // Get current departure to merge with updates
448 $current = $this->findModel($id);
449 if ($current) {
450 $merged = array_merge($current->toArray(), $updateData);
451 $departure = Departure::fromArray($merged);
452 $updateData['status'] = $departure->calculateStatus();
453 }
454 }
455
456 if (empty($updateData)) {
457 return false;
458 }
459
460 $formats = [];
461 foreach ($updateData as $value) {
462 if (is_int($value)) {
463 $formats[] = '%d';
464 } elseif (is_float($value)) {
465 $formats[] = '%f';
466 } else {
467 $formats[] = '%s';
468 }
469 }
470
471 return (bool) $this->wpdb->update(
472 $table,
473 $updateData,
474 ['id' => $id],
475 $formats,
476 ['%d']
477 );
478 }
479
480 /**
481 * Increment booked count
482 */
483 public function incrementBookedCount(int $id, int $amount = 1): bool
484 {
485 $table = esc_sql($this->table);
486
487 $this->wpdb->query($this->wpdb->prepare(
488 "UPDATE `{$table}`
489 SET booked_count = booked_count + %d,
490 updated_at = %s
491 WHERE id = %d",
492 $amount,
493 current_time('mysql'),
494 $id
495 ));
496
497 // Recalculate status
498 $departure = $this->findModel($id);
499 if ($departure) {
500 $this->update($id, ['status' => $departure->calculateStatus()]);
501 }
502
503 return true;
504 }
505
506 /**
507 * Decrement booked count
508 */
509 public function decrementBookedCount(int $id, int $amount = 1): bool
510 {
511 $table = esc_sql($this->table);
512
513 $this->wpdb->query($this->wpdb->prepare(
514 "UPDATE `{$table}`
515 SET booked_count = GREATEST(0, booked_count - %d),
516 updated_at = %s
517 WHERE id = %d",
518 $amount,
519 current_time('mysql'),
520 $id
521 ));
522
523 // Recalculate status
524 $departure = $this->findModel($id);
525 if ($departure) {
526 $this->update($id, ['status' => $departure->calculateStatus()]);
527 }
528
529 return true;
530 }
531
532 /**
533 * Delete a departure
534 * Only allowed if source is recurring_generated and booked_count is 0
535 */
536 public function delete(int $id): bool
537 {
538 $departure = $this->findModel($id);
539
540 if (!$departure) {
541 return false;
542 }
543
544 // Only allow deletion of recurring_generated departures with no bookings
545 if ($departure->source === 'recurring_generated' && $departure->booked_count === 0) {
546 $table = esc_sql($this->table);
547 return (bool) $this->wpdb->delete($table, ['id' => $id], ['%d']);
548 }
549
550 // Manual departures or departures with bookings cannot be deleted
551 return false;
552 }
553
554 /**
555 * Recalculate status for all departures (for cron job)
556 */
557 public function recalculateAllStatuses(): int
558 {
559 $table = esc_sql($this->table);
560 $today = date('Y-m-d');
561
562 // Update past departures - use end_date if available, otherwise start_date or date
563 $this->wpdb->query($this->wpdb->prepare(
564 "UPDATE `{$table}`
565 SET status = 'past', updated_at = %s
566 WHERE (
567 (end_date IS NOT NULL AND end_date != '' AND end_date < %s) OR
568 (end_date IS NULL OR end_date = '') AND (
569 (start_date IS NOT NULL AND start_date != '' AND start_date < %s) OR
570 (start_date IS NULL OR start_date = '') AND date < %s
571 )
572 )
573 AND status != 'cancelled'",
574 current_time('mysql'),
575 $today,
576 $today,
577 $today
578 ));
579
580 // Update full departures - check future dates only
581 $this->wpdb->query(
582 "UPDATE `{$table}`
583 SET status = 'full', updated_at = NOW()
584 WHERE booked_count >= max_capacity
585 AND max_capacity > 0
586 AND (
587 (end_date IS NOT NULL AND end_date != '' AND end_date >= CURDATE()) OR
588 (end_date IS NULL OR end_date = '') AND (
589 (start_date IS NOT NULL AND start_date != '' AND start_date >= CURDATE()) OR
590 (start_date IS NULL OR start_date = '') AND date >= CURDATE()
591 )
592 )
593 AND status NOT IN ('cancelled', 'past')"
594 );
595
596 // Update upcoming departures - check future dates only
597 $this->wpdb->query(
598 "UPDATE `{$table}`
599 SET status = 'upcoming', updated_at = NOW()
600 WHERE (
601 (end_date IS NOT NULL AND end_date != '' AND end_date >= CURDATE()) OR
602 (end_date IS NULL OR end_date = '') AND (
603 (start_date IS NOT NULL AND start_date != '' AND start_date >= CURDATE()) OR
604 (start_date IS NULL OR start_date = '') AND date >= CURDATE()
605 )
606 )
607 AND booked_count < max_capacity
608 AND status NOT IN ('cancelled', 'past', 'full')"
609 );
610
611 return $this->wpdb->rows_affected;
612 }
613
614 /**
615 * Check if table supports soft delete
616 */
617 protected function hasSoftDelete(): bool
618 {
619 return false;
620 }
621 }
622
623