PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
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.5.1, at app/Services/DepartureService.php

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