PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
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 2.0.1 All 84 releases
yatra / app / Services / DepartureService.php

DepartureService.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.6, at app/Services/DepartureService.php

774 lines 28.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\Services;
6
7 use Yatra\Repositories\DepartureRepository;
8 use Yatra\Repositories\BookingDepartureRepository;
9 use Yatra\Repositories\BookingRepository;
10 use Yatra\Repositories\TripRepository;
11 use Yatra\Models\Departure;
12 use Yatra\Services\CapacityService;
13
14 /**
15 * Departure Service
16 * Handles business logic for trip departures
17 */
18 class DepartureService
19 {
20 private DepartureRepository $repository;
21 private BookingDepartureRepository $bookingDepartureRepository;
22 private BookingRepository $bookingRepository;
23 private TripRepository $tripRepository;
24 private CapacityService $capacityService;
25
26 public function __construct(
27 DepartureRepository $repository,
28 ?BookingDepartureRepository $bookingDepartureRepository = null,
29 ?BookingRepository $bookingRepository = null,
30 ?TripRepository $tripRepository = null,
31 ?CapacityService $capacityService = null
32 ) {
33 $this->repository = $repository;
34 $this->bookingDepartureRepository = $bookingDepartureRepository ?? new BookingDepartureRepository();
35 $this->bookingRepository = $bookingRepository ?? new BookingRepository();
36 $this->tripRepository = $tripRepository ?? new TripRepository();
37 $this->capacityService = $capacityService ?? new CapacityService();
38 }
39
40 /**
41 * Create a departure (for admin editing - departures are normally auto-created)
42 */
43 public function create(array $data): int
44 {
45 // Validate required fields
46 if (empty($data['trip_id'])) {
47 throw new \InvalidArgumentException('Trip ID is required');
48 }
49
50 // Support both old 'date' and new 'start_date' format
51 $startDate = $data['start_date'] ?? $data['date'] ?? '';
52 if (empty($startDate)) {
53 throw new \InvalidArgumentException('Start date is required');
54 }
55
56 // Calculate capacity based on the date if not provided
57 if (empty($data['max_capacity'])) {
58 $data['max_capacity'] = $this->capacityService->getCapacityForDate(
59 (int) $data['trip_id'],
60 $startDate
61 );
62
63 // If still no capacity, throw an error
64 if ($data['max_capacity'] <= 0) {
65 throw new \InvalidArgumentException('No valid capacity found for the selected date. Please check availability settings.');
66 }
67 } elseif ((int) $data['max_capacity'] < 1) {
68 throw new \InvalidArgumentException('Max capacity must be at least 1');
69 }
70
71 // Validate date format
72 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
73 throw new \InvalidArgumentException('Invalid date format. Use YYYY-MM-DD');
74 }
75
76 // Calculate end_date if not provided
77 if (empty($data['end_date'])) {
78 $trip = $this->tripRepository->find((int) $data['trip_id']);
79 $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
80 $data['end_date'] = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
81 }
82
83 $data['start_date'] = $startDate;
84 $data['date'] = $startDate; // Keep for backward compatibility
85
86 // Check if departure already exists for this trip and start_date
87 $existing = $this->repository->findByTripIdAndStartDate(
88 (int) $data['trip_id'],
89 $startDate,
90 $data['time'] ?? null
91 );
92
93 if ($existing) {
94 throw new \InvalidArgumentException('A departure already exists for this trip and date');
95 }
96
97 // Set defaults
98 $data['source'] = $data['source'] ?? 'manual'; // Admin-created departures are 'manual'
99 $data['booked_count'] = $data['booked_count'] ?? 0;
100
101 // Create departure
102 $id = $this->repository->create($data);
103
104 // Trigger hook to sync capacity from availability
105 do_action('yatra_departure_saved', $id);
106
107 // Recalculate status
108 $departure = $this->repository->findModel($id);
109 if ($departure) {
110 $this->repository->update($id, ['status' => $departure->calculateStatus()]);
111 }
112
113 return $id;
114 }
115
116 /**
117 * Update a departure (for admin editing)
118 */
119 public function update(int $id, array $data): bool
120 {
121 $departure = $this->repository->findModel($id);
122
123 if (!$departure) {
124 throw new \InvalidArgumentException('Departure not found');
125 }
126
127 // Handle start_date and end_date
128 $startDate = $data['start_date'] ?? $data['date'] ?? null;
129
130 // Validate date format if provided
131 if ($startDate && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
132 throw new \InvalidArgumentException('Invalid date format. Use YYYY-MM-DD');
133 }
134
135 // Calculate end_date if start_date changed but end_date not provided
136 if ($startDate && $startDate !== ($departure->start_date ?: $departure->date)) {
137 if (empty($data['end_date'])) {
138 $trip = $this->tripRepository->find($departure->trip_id);
139 $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
140 $data['end_date'] = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
141 }
142 $data['start_date'] = $startDate;
143 $data['date'] = $startDate; // Keep in sync
144 }
145
146 // Check for duplicate if start_date is being changed
147 if ($startDate && $startDate !== ($departure->start_date ?: $departure->date)) {
148 $existing = $this->repository->findByTripIdAndStartDate(
149 $departure->trip_id,
150 $startDate,
151 $data['time'] ?? $departure->time
152 );
153
154 if ($existing && $existing->id !== $id) {
155 throw new \InvalidArgumentException('A departure already exists for this trip and date');
156 }
157 }
158
159 // Mark as manually edited by admin
160 if (!isset($data['source'])) {
161 $data['source'] = 'manual'; // Admin edits mark as manual
162 }
163
164 // Update departure
165 $result = $this->repository->update($id, $data);
166
167 // Trigger hook to sync capacity from availability
168 if ($result) {
169 do_action('yatra_departure_saved', $id);
170 }
171
172 // If status is being explicitly set to 'trash' (admin trash feature),
173 // skip automatic status recalculation so the trashed state is preserved.
174 if (isset($data['status']) && $data['status'] === 'trash') {
175 return $result;
176 }
177
178 // Recalculate status for all other updates
179 $departure = $this->repository->findModel($id);
180 if ($departure) {
181 $this->repository->update($id, ['status' => $departure->calculateStatus()]);
182 }
183
184 return $result;
185 }
186
187 /**
188 * Delete a departure
189 * Only allowed if source is recurring_generated and booked_count is 0
190 */
191 public function delete(int $id): bool
192 {
193 $departure = $this->repository->findModel($id);
194
195 if (!$departure) {
196 throw new \InvalidArgumentException('Departure not found');
197 }
198
199 // Only allow deletion of recurring_generated departures with no bookings
200 if ($departure->source === 'recurring_generated' && $departure->booked_count === 0) {
201 return $this->repository->delete($id);
202 }
203
204 // Manual departures or departures with bookings cannot be deleted
205 throw new \InvalidArgumentException('Cannot delete departure: Manual departures or departures with bookings cannot be deleted');
206 }
207
208 /**
209 * Increment booked count (when booking is created)
210 */
211 public function incrementBookedCount(int $id, int $amount = 1): bool
212 {
213 $departure = $this->repository->findModel($id);
214
215 if (!$departure) {
216 throw new \InvalidArgumentException('Departure not found');
217 }
218
219 // Check if capacity allows (only when max_capacity is set)
220 $currentBooked = (int) ($departure->booked_count ?? 0);
221 $maxCapacity = $departure->max_capacity !== null ? (int) $departure->max_capacity : 0;
222 if ($maxCapacity > 0 && ($currentBooked + $amount > $maxCapacity)) {
223 // Do not throw; just prevent exceeding capacity
224 return false;
225 }
226
227 $ok = $this->repository->incrementBookedCount($id, $amount);
228 if ($ok) {
229 $dep = $this->repository->findModel($id);
230 if ($dep) {
231 $st = $dep->calculateStatus();
232 if ($st !== $dep->status) {
233 $this->repository->update($id, ['status' => $st]);
234 }
235 }
236 }
237
238 return $ok;
239 }
240
241 /**
242 * Decrement booked count (when booking is cancelled)
243 */
244 public function decrementBookedCount(int $id, int $amount = 1): bool
245 {
246 $departure = $this->repository->findModel($id);
247
248 if (!$departure) {
249 throw new \InvalidArgumentException('Departure not found');
250 }
251
252 $ok = $this->repository->decrementBookedCount($id, $amount);
253 if ($ok) {
254 $dep = $this->repository->findModel($id);
255 if ($dep) {
256 $st = $dep->calculateStatus();
257 if ($st !== $dep->status) {
258 $this->repository->update($id, ['status' => $st]);
259 }
260 }
261 }
262
263 return $ok;
264 }
265
266 /**
267 * Get all departures across all trips
268 */
269 public function getAllDepartures(array $filters = []): array
270 {
271 return $this->repository->findAll($filters);
272 }
273
274 /**
275 * Get departures by trip ID
276 */
277 public function getByTripId(int $tripId, array $filters = []): array
278 {
279 return $this->repository->findByTripId($tripId, $filters);
280 }
281
282 /**
283 * Get past departures by trip ID
284 */
285 public function getPastByTripId(int $tripId, array $filters = []): array
286 {
287 return $this->repository->findPastByTripId($tripId, $filters);
288 }
289
290 /**
291 * Get upcoming departures by trip ID
292 */
293 public function getUpcomingByTripId(int $tripId, array $filters = []): array
294 {
295 return $this->repository->findUpcomingByTripId($tripId, $filters);
296 }
297
298 /**
299 * Get available dates for frontend
300 * Combines manual departures and dynamically generated recurring rule dates
301 *
302 * @param int $tripId Trip ID
303 * @param string $fromDate Start date (default: today)
304 * @param string $toDate End date (default: +12 months)
305 * @return array Available dates with pricing and capacity info
306 */
307 public function getAvailableDates(int $tripId, ?string $fromDate = null, ?string $toDate = null): array
308 {
309 $fromDate = $fromDate ?? date('Y-m-d');
310 $toDate = $toDate ?? date('Y-m-d', strtotime('+12 months'));
311
312 // Get all manual departures
313 $manualDepartures = $this->repository->findByTripId($tripId, [
314 'date_from' => $fromDate,
315 'date_to' => $toDate,
316 'include_past' => false,
317 ]);
318
319 // Get recurring rule service
320 $ruleRepository = new \Yatra\Repositories\RecurringRuleRepository();
321 $ruleService = new RecurringRuleService($ruleRepository, $this->repository);
322
323 // Generate dates from recurring rules
324 $recurringDates = $ruleService->generateDatesForTrip($tripId, $fromDate, $toDate);
325
326 // Combine and format
327 $availableDates = [];
328
329 // Add manual departures
330 foreach ($manualDepartures as $departure) {
331 if ($departure->isAvailable()) {
332 $availableDates[$departure->date] = [
333 'id' => $departure->id,
334 'date' => $departure->date,
335 'time' => $departure->time,
336 'max_capacity' => $departure->max_capacity,
337 'available_capacity' => $departure->max_capacity - $departure->booked_count,
338 'booked_count' => $departure->booked_count,
339 'status' => $departure->status,
340 'source' => $departure->source,
341 'price_override' => $departure->price_override,
342 'price_by_traveler_type' => $departure->price_by_traveler_type,
343 'is_full' => $departure->booked_count >= $departure->max_capacity,
344 ];
345 }
346 }
347
348 // Add recurring rule dates (only if no manual departure exists)
349 foreach ($recurringDates as $dateInfo) {
350 if (!isset($availableDates[$dateInfo['date']])) {
351 $availableDates[$dateInfo['date']] = [
352 'id' => null,
353 'date' => $dateInfo['date'],
354 'time' => null,
355 'max_capacity' => $dateInfo['max_capacity'],
356 'available_capacity' => $dateInfo['max_capacity'],
357 'booked_count' => 0,
358 'status' => 'upcoming',
359 'source' => 'recurring_rule',
360 'price_override' => $dateInfo['base_price'],
361 'price_by_traveler_type' => $dateInfo['pricing_by_traveler_type'],
362 'is_full' => false,
363 'rule_id' => $dateInfo['rule_id'],
364 ];
365 }
366 }
367
368 // Sort by date
369 ksort($availableDates);
370
371 return array_values($availableDates);
372 }
373
374 /**
375 * Recalculate all departure statuses (for cron job)
376 */
377 public function recalculateAllStatuses(): int
378 {
379 return $this->repository->recalculateAllStatuses();
380 }
381
382 /**
383 * Find or create a departure for a booking
384 * If departure doesn't exist, creates it automatically
385 *
386 * @param int $tripId Trip ID
387 * @param string $startDate Start date (YYYY-MM-DD)
388 * @param string $endDate End date (YYYY-MM-DD)
389 * @param int $travelersCount Number of travelers in the booking
390 * @param int|null $defaultMaxCapacity Default max capacity if creating new departure (null = unlimited)
391 * @param string|null $time Time in HH:MM:SS format (optional)
392 * @return Departure The departure (existing or newly created)
393 * @throws \Exception
394 */
395 public function findOrCreateForBooking(int $tripId, string $startDate, string $endDate, int $travelersCount = 0, ?int $defaultMaxCapacity = null, ?string $time = null): Departure
396 {
397 // Get capacity based on priority
398 $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate);
399
400 // If no capacity found from availability or rules, use the provided default
401 if ($maxCapacity <= 0 && $defaultMaxCapacity !== null) {
402 $maxCapacity = $defaultMaxCapacity;
403 }
404 // Validate date format
405 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
406 throw new \InvalidArgumentException('Invalid start date format. Use YYYY-MM-DD');
407 }
408 if (!empty($endDate) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
409 throw new \InvalidArgumentException('Invalid end date format. Use YYYY-MM-DD');
410 }
411
412 // Try to find existing departure by start_date and time
413 $departure = $this->repository->findByTripIdAndStartDate($tripId, $startDate, $time);
414
415 if ($departure) {
416 // Departure exists, sync capacity from availability before returning
417 if ($maxCapacity > 0 && $departure->max_capacity !== $maxCapacity) {
418 $this->repository->update($departure->id, ['max_capacity' => $maxCapacity]);
419 $departure->max_capacity = $maxCapacity;
420 }
421 return $departure;
422 }
423
424 // Departure doesn't exist, create it
425 $trip = $this->tripRepository->find($tripId);
426
427 // Calculate end_date if not provided
428 if (empty($endDate)) {
429 $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
430 $endDate = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
431 }
432
433 // Keep capacity from availability/rules/trip (resolved above). Only use caller default if still unknown.
434 if ($maxCapacity <= 0 && $defaultMaxCapacity !== null && $defaultMaxCapacity > 0) {
435 $maxCapacity = $defaultMaxCapacity;
436 }
437 if ($maxCapacity <= 0) {
438 $maxCapacity = $trip ? max(1, (int) ($trip->max_travelers ?? $trip->max_travellers ?? 1)) : 1;
439 }
440
441 // Trip already loaded for end_date / capacity fallbacks
442 $defaultTime = $time;
443
444 // If no time provided, try to get from trip settings or use default
445 if (!$defaultTime && $trip) {
446 // Check if trip has a default departure time
447 $defaultTime = $trip->departure_time ?? '09:00'; // Default to 9:00 AM
448 }
449
450 $departureData = [
451 'trip_id' => $tripId,
452 'start_date' => $startDate,
453 'end_date' => $endDate,
454 'date' => $startDate, // Keep for backward compatibility
455 'time' => $defaultTime, // Add time (default or provided)
456 'max_capacity' => $maxCapacity,
457 'booked_count' => 0, // Will be incremented after creation
458 'total_revenue' => 0.00, // Initialize revenue
459 'source' => 'booking_created', // Created from booking
460 'status' => 'upcoming', // Will be recalculated
461 ];
462
463 $departureId = $this->repository->create($departureData);
464 $departure = $this->repository->findModel($departureId);
465
466 if (!$departure) {
467 throw new \RuntimeException('Failed to create departure');
468 }
469
470 return $departure;
471 }
472
473 /**
474 * Link a booking to a departure
475 *
476 * @param int $bookingId Booking ID
477 * @param int $departureId Departure ID
478 * @return bool Success
479 */
480 public function linkBookingToDeparture(int $bookingId, int $departureId): bool
481 {
482 $result = $this->bookingDepartureRepository->link($bookingId, $departureId);
483
484 if ($result) {
485 $this->recalculateDepartureRevenue($departureId);
486 $departure = $this->repository->findModel($departureId);
487 if ($departure) {
488 $newStatus = $departure->calculateStatus();
489 if ($newStatus !== $departure->status) {
490 $this->repository->update($departureId, ['status' => $newStatus]);
491 }
492 }
493 }
494
495 return $result;
496 }
497
498 /**
499 * Unlink a booking from a departure
500 * If departure has no more bookings, mark it as cancelled
501 *
502 * @param int $bookingId Booking ID
503 * @param int|null $departureId Optional departure ID
504 * @return bool Success
505 */
506 public function unlinkBookingFromDeparture(int $bookingId, ?int $departureId = null): bool
507 {
508 $booking = $this->bookingRepository->find($bookingId);
509 $travelersCount = $booking ? max(0, (int) ($booking->travelers_count ?? 0)) : 0;
510
511 // Get departure ID if not provided
512 if ($departureId === null) {
513 $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
514 if ($departureId === null) {
515 return true; // No link exists
516 }
517 }
518
519 // Unlink booking
520 $result = $this->bookingDepartureRepository->unlink($bookingId, $departureId);
521
522 if (!$result) {
523 return false;
524 }
525
526 if ($travelersCount > 0) {
527 $this->decrementBookedCount($departureId, $travelersCount);
528 }
529
530 // Recalculate total revenue for the departure
531 $this->recalculateDepartureRevenue($departureId);
532
533 // Check if departure has any more bookings
534 $bookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($departureId);
535
536 if ($bookingCount === 0) {
537 // Mark departure as cancelled with note
538 $this->cancelDeparture($departureId, 'Cancelled - All bookings removed');
539 } else {
540 // Recalculate status
541 $departure = $this->repository->findModel($departureId);
542 if ($departure) {
543 $this->repository->update($departureId, ['status' => $departure->calculateStatus()]);
544 }
545 }
546
547 return true;
548 }
549
550 /**
551 * Cancel a departure (mark as cancelled with note, never delete)
552 *
553 * @param int $departureId Departure ID
554 * @param string $note Cancellation note
555 * @return bool Success
556 */
557 public function cancelDeparture(int $departureId, string $note): bool
558 {
559 $departure = $this->repository->findModel($departureId);
560
561 if (!$departure) {
562 return false;
563 }
564
565 // Update status and notes
566 return $this->repository->update($departureId, [
567 'status' => 'cancelled',
568 'notes' => $note,
569 ]);
570 }
571
572 /**
573 * Handle booking date change
574 * Creates new departure and cancels old one
575 *
576 * @param int $bookingId Booking ID
577 * @param string $newStartDate New start date
578 * @param string $newEndDate New end date
579 * @return array {success: bool, new_departure_id: int, old_departure_id: int|null}
580 */
581 public function handleBookingDateChange(int $bookingId, string $newStartDate, string $newEndDate): array
582 {
583 // Get booking to find trip_id
584 $booking = $this->bookingRepository->find($bookingId);
585 if (!$booking) {
586 throw new \InvalidArgumentException('Booking not found');
587 }
588
589 $tripId = (int) $booking->trip_id;
590 $oldDepartureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
591
592 // Get trip for max capacity
593 $trip = $this->tripRepository->find($tripId);
594 // Get max capacity from trip's max_travelers, or use default
595 $maxCapacity = null;
596 if ($trip && !empty($trip->max_travelers)) {
597 $maxCapacity = (int) $trip->max_travelers;
598 }
599
600 // Find or create new departure
601 $newDeparture = $this->findOrCreateForBooking($tripId, $newStartDate, $newEndDate, 0, $maxCapacity);
602
603 // Link booking to new departure
604 $this->bookingDepartureRepository->updateDepartureForBooking($bookingId, $newDeparture->id);
605
606 // Increment booked count for new departure
607 $travelersCount = (int) ($booking->travelers_count ?? 0);
608 $this->incrementBookedCount($newDeparture->id, $travelersCount);
609
610 // Handle old departure (link was moved in updateDepartureForBooking; adjust counts explicitly)
611 if ($oldDepartureId) {
612 if ($travelersCount > 0) {
613 $this->decrementBookedCount($oldDepartureId, $travelersCount);
614 }
615 $oldBookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($oldDepartureId);
616 if ($oldBookingCount === 0) {
617 $this->cancelDeparture($oldDepartureId, "Cancelled - Booking date changed (Booking ID: {$bookingId})");
618 }
619 }
620
621 return [
622 'success' => true,
623 'new_departure_id' => $newDeparture->id,
624 'old_departure_id' => $oldDepartureId,
625 ];
626 }
627
628 /**
629 * Get all bookings for a departure
630 *
631 * @param int $departureId Departure ID
632 * @return array Array of booking objects
633 */
634 public function getBookingsForDeparture(int $departureId): array
635 {
636 // Use repository helper to get booking IDs for this departure
637 $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
638
639 if (empty($bookingIds)) {
640 return [];
641 }
642
643 $bookings = [];
644 foreach ($bookingIds as $bookingId) {
645 $bookingId = (int) $bookingId;
646 if ($bookingId <= 0) {
647 continue;
648 }
649 $booking = $this->bookingRepository->find($bookingId);
650 if ($booking) {
651 $bookings[] = $booking;
652 }
653 }
654
655 return $bookings;
656 }
657
658 /**
659 * Get departure for a booking
660 *
661 * @param int $bookingId Booking ID
662 * @return Departure|null Departure or null
663 */
664 public function getDepartureForBooking(int $bookingId): ?Departure
665 {
666 $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
667
668 if ($departureId === null) {
669 return null;
670 }
671
672 return $this->repository->findModel($departureId);
673 }
674
675 /**
676 * Check if a date matches a recurring rule
677 *
678 * @param string $date Date to check (YYYY-MM-DD)
679 * @param object $rule Recurring rule object
680 * @return bool True if date matches the rule
681 */
682 private function dateMatchesRecurringRule(string $date, object $rule): bool
683 {
684 // Check date range
685 if (!empty($rule->start_date) && $date < $rule->start_date) {
686 return false;
687 }
688 if (!empty($rule->end_date) && $date > $rule->end_date) {
689 return false;
690 }
691
692 $dayOfWeek = (int) date('w', strtotime($date)); // 0 = Sunday, 6 = Saturday
693 $recurrenceType = $rule->recurrence_type ?? 'daily';
694 $weekdays = is_string($rule->weekdays) ? json_decode($rule->weekdays, true) : ($rule->weekdays ?? []);
695
696 switch ($recurrenceType) {
697 case 'daily':
698 return true;
699
700 case 'weekly':
701 return in_array($dayOfWeek, $weekdays, true);
702
703 case 'monthly':
704 // Check if it's the same day of month
705 $ruleDay = !empty($rule->start_date) ? (int) date('d', strtotime($rule->start_date)) : null;
706 $checkDay = (int) date('d', strtotime($date));
707 return $ruleDay === null || $ruleDay === $checkDay;
708
709 case 'custom_days':
710 return in_array($dayOfWeek, $weekdays, true);
711
712 default:
713 return false;
714 }
715 }
716
717 /**
718 * Get the departure repository
719 *
720 * @return \Yatra\Repositories\DepartureRepository
721 */
722 public function getRepository(): \Yatra\Repositories\DepartureRepository
723 {
724 return $this->repository;
725 }
726
727 /**
728 * Recalculate total revenue for a departure from all linked bookings
729 *
730 * @param int $departureId Departure ID
731 * @return float Total revenue
732 */
733 public function recalculateDepartureRevenue(int $departureId): float
734 {
735 // Use repository helper to fetch booking IDs
736 $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
737
738 if (empty($bookingIds)) {
739 $this->repository->update($departureId, ['total_revenue' => 0.00]);
740 return 0.00;
741 }
742
743 $totalRevenue = 0.00;
744
745 foreach ($bookingIds as $bookingId) {
746 $bookingId = (int) $bookingId;
747 if ($bookingId <= 0) {
748 continue;
749 }
750 $booking = $this->bookingRepository->find($bookingId);
751 if ($booking) {
752 // Only count confirmed/pending bookings, exclude cancelled/refunded
753 if (!in_array($booking->status ?? '', ['cancelled', 'refunded', 'failed'], true)) {
754 // If total_amount is not set, try to calculate from price * travelers
755 if (!empty($booking->total_amount)) {
756 $totalRevenue += (float) $booking->total_amount;
757 } else if (!empty($booking->price) && !empty($booking->traveler_count)) {
758 $totalRevenue += ((float) $booking->price * (int) $booking->traveler_count);
759 } else if (!empty($booking->price)) {
760 // Fallback to just price if traveler count not available
761 $totalRevenue += (float) $booking->price;
762 }
763 }
764 }
765 }
766
767 // Update departure with calculated revenue
768 $this->repository->update($departureId, ['total_revenue' => $totalRevenue]);
769
770 return $totalRevenue;
771 }
772 }
773
774