PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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 / Services / DepartureService.php

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

799 lines 29.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\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 *
190 * Allowed once the departure no longer has any booking attached, whether it
191 * was created automatically or by hand — an operator who adds a departure by
192 * mistake must be able to remove it again.
193 */
194 public function delete(int $id): bool
195 {
196 $departure = $this->repository->findModel($id);
197
198 if (!$departure) {
199 throw new \InvalidArgumentException('Departure not found');
200 }
201
202 // Bookings themselves are the source of truth here, not `booked_count`:
203 // that counter can drift upwards (a cancelled or expired booking does not
204 // always decrement it), which would otherwise leave a departure
205 // permanently undeletable long after its last booking went away.
206 //
207 // This is the ONLY thing standing between a departure and deletion. The
208 // previous guard also required source === 'recurring_generated', a value
209 // this plugin never writes (departures are `booking_created` or `manual`,
210 // see Departure::$source), so no departure was ever deletable at all.
211 if (!empty($this->bookingRepository->findByDepartureId($id))) {
212 throw new \InvalidArgumentException(
213 __('Cannot delete departure: it still has bookings attached.', 'yatra')
214 );
215 }
216
217 return $this->repository->delete($id);
218 }
219
220 /**
221 * Increment booked count (when booking is created).
222 *
223 * @param bool $force When true, bypasses the capacity guard so the
224 * increment lands even if it would exceed `max_capacity`. Use
225 * only for after-the-fact reconciliation paths — most commonly
226 * external-channel bookings (Viator / GetYourGuide / any OTA
227 * webhook) where the seat has ALREADY been sold on the OTA
228 * side. Refusing to record the increment would hide the oversell
229 * from the operator and break reconciliation. Direct-checkout
230 * callers should leave this false to keep overbooking protection.
231 */
232 public function incrementBookedCount(int $id, int $amount = 1, bool $force = false): bool
233 {
234 $departure = $this->repository->findModel($id);
235
236 if (!$departure) {
237 throw new \InvalidArgumentException('Departure not found');
238 }
239
240 // Capacity pre-check is the same guard as before — but only
241 // for non-forced callers. Forced callers (OTA ingest) skip it
242 // entirely and rely on the repository to write unconditionally.
243 if (!$force) {
244 $currentBooked = (int) ($departure->booked_count ?? 0);
245 $maxCapacity = $departure->max_capacity !== null ? (int) $departure->max_capacity : 0;
246 if ($maxCapacity > 0 && ($currentBooked + $amount > $maxCapacity)) {
247 // Do not throw; just prevent exceeding capacity.
248 return false;
249 }
250 }
251
252 $ok = $this->repository->incrementBookedCount($id, $amount, $force);
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 * Decrement booked count (when booking is cancelled)
268 */
269 public function decrementBookedCount(int $id, int $amount = 1): bool
270 {
271 $departure = $this->repository->findModel($id);
272
273 if (!$departure) {
274 throw new \InvalidArgumentException('Departure not found');
275 }
276
277 $ok = $this->repository->decrementBookedCount($id, $amount);
278 if ($ok) {
279 $dep = $this->repository->findModel($id);
280 if ($dep) {
281 $st = $dep->calculateStatus();
282 if ($st !== $dep->status) {
283 $this->repository->update($id, ['status' => $st]);
284 }
285 }
286 }
287
288 return $ok;
289 }
290
291 /**
292 * Get all departures across all trips
293 */
294 public function getAllDepartures(array $filters = []): array
295 {
296 return $this->repository->findAll($filters);
297 }
298
299 /**
300 * Get departures by trip ID
301 */
302 public function getByTripId(int $tripId, array $filters = []): array
303 {
304 return $this->repository->findByTripId($tripId, $filters);
305 }
306
307 /**
308 * Get past departures by trip ID
309 */
310 public function getPastByTripId(int $tripId, array $filters = []): array
311 {
312 return $this->repository->findPastByTripId($tripId, $filters);
313 }
314
315 /**
316 * Get upcoming departures by trip ID
317 */
318 public function getUpcomingByTripId(int $tripId, array $filters = []): array
319 {
320 return $this->repository->findUpcomingByTripId($tripId, $filters);
321 }
322
323 /**
324 * Get available dates for frontend
325 * Combines manual departures and dynamically generated recurring rule dates
326 *
327 * @param int $tripId Trip ID
328 * @param string $fromDate Start date (default: today)
329 * @param string $toDate End date (default: +12 months)
330 * @return array Available dates with pricing and capacity info
331 */
332 public function getAvailableDates(int $tripId, ?string $fromDate = null, ?string $toDate = null): array
333 {
334 $fromDate = $fromDate ?? date('Y-m-d');
335 $toDate = $toDate ?? date('Y-m-d', strtotime('+12 months'));
336
337 // Get all manual departures
338 $manualDepartures = $this->repository->findByTripId($tripId, [
339 'date_from' => $fromDate,
340 'date_to' => $toDate,
341 'include_past' => false,
342 ]);
343
344 // Get recurring rule service
345 $ruleRepository = new \Yatra\Repositories\RecurringRuleRepository();
346 $ruleService = new RecurringRuleService($ruleRepository, $this->repository);
347
348 // Generate dates from recurring rules
349 $recurringDates = $ruleService->generateDatesForTrip($tripId, $fromDate, $toDate);
350
351 // Combine and format
352 $availableDates = [];
353
354 // Add manual departures
355 foreach ($manualDepartures as $departure) {
356 if ($departure->isAvailable()) {
357 $availableDates[$departure->date] = [
358 'id' => $departure->id,
359 'date' => $departure->date,
360 'time' => $departure->time,
361 'max_capacity' => $departure->max_capacity,
362 'available_capacity' => $departure->max_capacity - $departure->booked_count,
363 'booked_count' => $departure->booked_count,
364 'status' => $departure->status,
365 'source' => $departure->source,
366 'price_override' => $departure->price_override,
367 'price_by_traveler_type' => $departure->price_by_traveler_type,
368 'is_full' => $departure->booked_count >= $departure->max_capacity,
369 ];
370 }
371 }
372
373 // Add recurring rule dates (only if no manual departure exists)
374 foreach ($recurringDates as $dateInfo) {
375 if (!isset($availableDates[$dateInfo['date']])) {
376 $availableDates[$dateInfo['date']] = [
377 'id' => null,
378 'date' => $dateInfo['date'],
379 'time' => null,
380 'max_capacity' => $dateInfo['max_capacity'],
381 'available_capacity' => $dateInfo['max_capacity'],
382 'booked_count' => 0,
383 'status' => 'upcoming',
384 'source' => 'recurring_rule',
385 'price_override' => $dateInfo['base_price'],
386 'price_by_traveler_type' => $dateInfo['pricing_by_traveler_type'],
387 'is_full' => false,
388 'rule_id' => $dateInfo['rule_id'],
389 ];
390 }
391 }
392
393 // Sort by date
394 ksort($availableDates);
395
396 return array_values($availableDates);
397 }
398
399 /**
400 * Recalculate all departure statuses (for cron job)
401 */
402 public function recalculateAllStatuses(): int
403 {
404 return $this->repository->recalculateAllStatuses();
405 }
406
407 /**
408 * Find or create a departure for a booking
409 * If departure doesn't exist, creates it automatically
410 *
411 * @param int $tripId Trip ID
412 * @param string $startDate Start date (YYYY-MM-DD)
413 * @param string $endDate End date (YYYY-MM-DD)
414 * @param int $travelersCount Number of travelers in the booking
415 * @param int|null $defaultMaxCapacity Default max capacity if creating new departure (null = unlimited)
416 * @param string|null $time Time in HH:MM:SS format (optional)
417 * @return Departure The departure (existing or newly created)
418 * @throws \Exception
419 */
420 public function findOrCreateForBooking(int $tripId, string $startDate, string $endDate, int $travelersCount = 0, ?int $defaultMaxCapacity = null, ?string $time = null): Departure
421 {
422 // Get capacity based on priority
423 $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate);
424
425 // If no capacity found from availability or rules, use the provided default
426 if ($maxCapacity <= 0 && $defaultMaxCapacity !== null) {
427 $maxCapacity = $defaultMaxCapacity;
428 }
429 // Validate date format
430 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
431 throw new \InvalidArgumentException('Invalid start date format. Use YYYY-MM-DD');
432 }
433 if (!empty($endDate) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
434 throw new \InvalidArgumentException('Invalid end date format. Use YYYY-MM-DD');
435 }
436
437 // Try to find existing departure by start_date and time
438 $departure = $this->repository->findByTripIdAndStartDate($tripId, $startDate, $time);
439
440 if ($departure) {
441 // Departure exists, sync capacity from availability before returning
442 if ($maxCapacity > 0 && $departure->max_capacity !== $maxCapacity) {
443 $this->repository->update($departure->id, ['max_capacity' => $maxCapacity]);
444 $departure->max_capacity = $maxCapacity;
445 }
446 return $departure;
447 }
448
449 // Departure doesn't exist, create it
450 $trip = $this->tripRepository->find($tripId);
451
452 // Calculate end_date if not provided
453 if (empty($endDate)) {
454 $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
455 $endDate = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
456 }
457
458 // Keep capacity from availability/rules/trip (resolved above). Only use caller default if still unknown.
459 if ($maxCapacity <= 0 && $defaultMaxCapacity !== null && $defaultMaxCapacity > 0) {
460 $maxCapacity = $defaultMaxCapacity;
461 }
462 if ($maxCapacity <= 0) {
463 $maxCapacity = $trip ? max(1, (int) ($trip->max_travelers ?? $trip->max_travellers ?? 1)) : 1;
464 }
465
466 // Trip already loaded for end_date / capacity fallbacks
467 $defaultTime = $time;
468
469 // If no time provided, try to get from trip settings or use default
470 if (!$defaultTime && $trip) {
471 // Check if trip has a default departure time
472 $defaultTime = $trip->departure_time ?? '09:00'; // Default to 9:00 AM
473 }
474
475 $departureData = [
476 'trip_id' => $tripId,
477 'start_date' => $startDate,
478 'end_date' => $endDate,
479 'date' => $startDate, // Keep for backward compatibility
480 'time' => $defaultTime, // Add time (default or provided)
481 'max_capacity' => $maxCapacity,
482 'booked_count' => 0, // Will be incremented after creation
483 'total_revenue' => 0.00, // Initialize revenue
484 'source' => 'booking_created', // Created from booking
485 'status' => 'upcoming', // Will be recalculated
486 ];
487
488 $departureId = $this->repository->create($departureData);
489 $departure = $this->repository->findModel($departureId);
490
491 if (!$departure) {
492 throw new \RuntimeException('Failed to create departure');
493 }
494
495 return $departure;
496 }
497
498 /**
499 * Link a booking to a departure
500 *
501 * @param int $bookingId Booking ID
502 * @param int $departureId Departure ID
503 * @return bool Success
504 */
505 public function linkBookingToDeparture(int $bookingId, int $departureId): bool
506 {
507 $result = $this->bookingDepartureRepository->link($bookingId, $departureId);
508
509 if ($result) {
510 $this->recalculateDepartureRevenue($departureId);
511 $departure = $this->repository->findModel($departureId);
512 if ($departure) {
513 $newStatus = $departure->calculateStatus();
514 if ($newStatus !== $departure->status) {
515 $this->repository->update($departureId, ['status' => $newStatus]);
516 }
517 }
518 }
519
520 return $result;
521 }
522
523 /**
524 * Unlink a booking from a departure
525 * If departure has no more bookings, mark it as cancelled
526 *
527 * @param int $bookingId Booking ID
528 * @param int|null $departureId Optional departure ID
529 * @return bool Success
530 */
531 public function unlinkBookingFromDeparture(int $bookingId, ?int $departureId = null): bool
532 {
533 $booking = $this->bookingRepository->find($bookingId);
534 $travelersCount = $booking ? max(0, (int) ($booking->travelers_count ?? 0)) : 0;
535
536 // Get departure ID if not provided
537 if ($departureId === null) {
538 $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
539 if ($departureId === null) {
540 return true; // No link exists
541 }
542 }
543
544 // Unlink booking
545 $result = $this->bookingDepartureRepository->unlink($bookingId, $departureId);
546
547 if (!$result) {
548 return false;
549 }
550
551 if ($travelersCount > 0) {
552 $this->decrementBookedCount($departureId, $travelersCount);
553 }
554
555 // Recalculate total revenue for the departure
556 $this->recalculateDepartureRevenue($departureId);
557
558 // Check if departure has any more bookings
559 $bookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($departureId);
560
561 if ($bookingCount === 0) {
562 // Mark departure as cancelled with note
563 $this->cancelDeparture($departureId, 'Cancelled - All bookings removed');
564 } else {
565 // Recalculate status
566 $departure = $this->repository->findModel($departureId);
567 if ($departure) {
568 $this->repository->update($departureId, ['status' => $departure->calculateStatus()]);
569 }
570 }
571
572 return true;
573 }
574
575 /**
576 * Cancel a departure (mark as cancelled with note, never delete)
577 *
578 * @param int $departureId Departure ID
579 * @param string $note Cancellation note
580 * @return bool Success
581 */
582 public function cancelDeparture(int $departureId, string $note): bool
583 {
584 $departure = $this->repository->findModel($departureId);
585
586 if (!$departure) {
587 return false;
588 }
589
590 // Update status and notes
591 return $this->repository->update($departureId, [
592 'status' => 'cancelled',
593 'notes' => $note,
594 ]);
595 }
596
597 /**
598 * Handle booking date change
599 * Creates new departure and cancels old one
600 *
601 * @param int $bookingId Booking ID
602 * @param string $newStartDate New start date
603 * @param string $newEndDate New end date
604 * @return array {success: bool, new_departure_id: int, old_departure_id: int|null}
605 */
606 public function handleBookingDateChange(int $bookingId, string $newStartDate, string $newEndDate): array
607 {
608 // Get booking to find trip_id
609 $booking = $this->bookingRepository->find($bookingId);
610 if (!$booking) {
611 throw new \InvalidArgumentException('Booking not found');
612 }
613
614 $tripId = (int) $booking->trip_id;
615 $oldDepartureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
616
617 // Get trip for max capacity
618 $trip = $this->tripRepository->find($tripId);
619 // Get max capacity from trip's max_travelers, or use default
620 $maxCapacity = null;
621 if ($trip && !empty($trip->max_travelers)) {
622 $maxCapacity = (int) $trip->max_travelers;
623 }
624
625 // Find or create new departure
626 $newDeparture = $this->findOrCreateForBooking($tripId, $newStartDate, $newEndDate, 0, $maxCapacity);
627
628 // Link booking to new departure
629 $this->bookingDepartureRepository->updateDepartureForBooking($bookingId, $newDeparture->id);
630
631 // Increment booked count for new departure
632 $travelersCount = (int) ($booking->travelers_count ?? 0);
633 $this->incrementBookedCount($newDeparture->id, $travelersCount);
634
635 // Handle old departure (link was moved in updateDepartureForBooking; adjust counts explicitly)
636 if ($oldDepartureId) {
637 if ($travelersCount > 0) {
638 $this->decrementBookedCount($oldDepartureId, $travelersCount);
639 }
640 $oldBookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($oldDepartureId);
641 if ($oldBookingCount === 0) {
642 $this->cancelDeparture($oldDepartureId, "Cancelled - Booking date changed (Booking ID: {$bookingId})");
643 }
644 }
645
646 return [
647 'success' => true,
648 'new_departure_id' => $newDeparture->id,
649 'old_departure_id' => $oldDepartureId,
650 ];
651 }
652
653 /**
654 * Get all bookings for a departure
655 *
656 * @param int $departureId Departure ID
657 * @return array Array of booking objects
658 */
659 public function getBookingsForDeparture(int $departureId): array
660 {
661 // Use repository helper to get booking IDs for this departure
662 $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
663
664 if (empty($bookingIds)) {
665 return [];
666 }
667
668 $bookings = [];
669 foreach ($bookingIds as $bookingId) {
670 $bookingId = (int) $bookingId;
671 if ($bookingId <= 0) {
672 continue;
673 }
674 $booking = $this->bookingRepository->find($bookingId);
675 if ($booking) {
676 $bookings[] = $booking;
677 }
678 }
679
680 return $bookings;
681 }
682
683 /**
684 * Get departure for a booking
685 *
686 * @param int $bookingId Booking ID
687 * @return Departure|null Departure or null
688 */
689 public function getDepartureForBooking(int $bookingId): ?Departure
690 {
691 $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
692
693 if ($departureId === null) {
694 return null;
695 }
696
697 return $this->repository->findModel($departureId);
698 }
699
700 /**
701 * Check if a date matches a recurring rule
702 *
703 * @param string $date Date to check (YYYY-MM-DD)
704 * @param object $rule Recurring rule object
705 * @return bool True if date matches the rule
706 */
707 private function dateMatchesRecurringRule(string $date, object $rule): bool
708 {
709 // Check date range
710 if (!empty($rule->start_date) && $date < $rule->start_date) {
711 return false;
712 }
713 if (!empty($rule->end_date) && $date > $rule->end_date) {
714 return false;
715 }
716
717 $dayOfWeek = (int) date('w', strtotime($date)); // 0 = Sunday, 6 = Saturday
718 $recurrenceType = $rule->recurrence_type ?? 'daily';
719 $weekdays = is_string($rule->weekdays) ? json_decode($rule->weekdays, true) : ($rule->weekdays ?? []);
720
721 switch ($recurrenceType) {
722 case 'daily':
723 return true;
724
725 case 'weekly':
726 return in_array($dayOfWeek, $weekdays, true);
727
728 case 'monthly':
729 // Check if it's the same day of month
730 $ruleDay = !empty($rule->start_date) ? (int) date('d', strtotime($rule->start_date)) : null;
731 $checkDay = (int) date('d', strtotime($date));
732 return $ruleDay === null || $ruleDay === $checkDay;
733
734 case 'custom_days':
735 return in_array($dayOfWeek, $weekdays, true);
736
737 default:
738 return false;
739 }
740 }
741
742 /**
743 * Get the departure repository
744 *
745 * @return \Yatra\Repositories\DepartureRepository
746 */
747 public function getRepository(): \Yatra\Repositories\DepartureRepository
748 {
749 return $this->repository;
750 }
751
752 /**
753 * Recalculate total revenue for a departure from all linked bookings
754 *
755 * @param int $departureId Departure ID
756 * @return float Total revenue
757 */
758 public function recalculateDepartureRevenue(int $departureId): float
759 {
760 // Use repository helper to fetch booking IDs
761 $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
762
763 if (empty($bookingIds)) {
764 $this->repository->update($departureId, ['total_revenue' => 0.00]);
765 return 0.00;
766 }
767
768 $totalRevenue = 0.00;
769
770 foreach ($bookingIds as $bookingId) {
771 $bookingId = (int) $bookingId;
772 if ($bookingId <= 0) {
773 continue;
774 }
775 $booking = $this->bookingRepository->find($bookingId);
776 if ($booking) {
777 // Only count confirmed/pending bookings, exclude cancelled/refunded
778 if (!in_array($booking->status ?? '', ['cancelled', 'refunded', 'failed'], true)) {
779 // If total_amount is not set, try to calculate from price * travelers
780 if (!empty($booking->total_amount)) {
781 $totalRevenue += (float) $booking->total_amount;
782 } else if (!empty($booking->price) && !empty($booking->traveler_count)) {
783 $totalRevenue += ((float) $booking->price * (int) $booking->traveler_count);
784 } else if (!empty($booking->price)) {
785 // Fallback to just price if traveler count not available
786 $totalRevenue += (float) $booking->price;
787 }
788 }
789 }
790 }
791
792 // Update departure with calculated revenue
793 $this->repository->update($departureId, ['total_revenue' => $totalRevenue]);
794
795 return $totalRevenue;
796 }
797 }
798
799