# yatra/3.0.11/app/Services/DepartureService.php

Yatra – Travel Booking &amp; Tour Operator Software, version 3.0.11. 799 lines.

- Page: https://pluginprobe.com/plugins/yatra/3.0.11/code/app/Services/DepartureService.php
- Raw: https://pluginprobe.com/plugins/yatra/3.0.11/raw/app/Services/DepartureService.php
- Modified: 2026-07-20T12:05:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/yatra/3.0.11/code/app/Services/DepartureService.php#L10-L20`.

```php
<?php

declare(strict_types=1);

namespace Yatra\Services;

use Yatra\Repositories\DepartureRepository;
use Yatra\Repositories\BookingDepartureRepository;
use Yatra\Repositories\BookingRepository;
use Yatra\Repositories\TripRepository;
use Yatra\Models\Departure;
use Yatra\Services\CapacityService;

/**
 * Departure Service
 * Handles business logic for trip departures
 */
class DepartureService
{
    private DepartureRepository $repository;
    private BookingDepartureRepository $bookingDepartureRepository;
    private BookingRepository $bookingRepository;
    private TripRepository $tripRepository;
    private CapacityService $capacityService;

    public function __construct(
        DepartureRepository $repository,
        ?BookingDepartureRepository $bookingDepartureRepository = null,
        ?BookingRepository $bookingRepository = null,
        ?TripRepository $tripRepository = null,
        ?CapacityService $capacityService = null
    ) {
        $this->repository = $repository;
        $this->bookingDepartureRepository = $bookingDepartureRepository ?? new BookingDepartureRepository();
        $this->bookingRepository = $bookingRepository ?? new BookingRepository();
        $this->tripRepository = $tripRepository ?? new TripRepository();
        $this->capacityService = $capacityService ?? new CapacityService();
    }

    /**
     * Create a departure (for admin editing - departures are normally auto-created)
     */
    public function create(array $data): int
    {
        // Validate required fields
        if (empty($data['trip_id'])) {
            throw new \InvalidArgumentException('Trip ID is required');
        }
        
        // Support both old 'date' and new 'start_date' format
        $startDate = $data['start_date'] ?? $data['date'] ?? '';
        if (empty($startDate)) {
            throw new \InvalidArgumentException('Start date is required');
        }
        
        // Calculate capacity based on the date if not provided
        if (empty($data['max_capacity'])) {
            $data['max_capacity'] = $this->capacityService->getCapacityForDate(
                (int) $data['trip_id'],
                $startDate
            );
            
            // If still no capacity, throw an error
            if ($data['max_capacity'] <= 0) {
                throw new \InvalidArgumentException('No valid capacity found for the selected date. Please check availability settings.');
            }
        } elseif ((int) $data['max_capacity'] < 1) {
            throw new \InvalidArgumentException('Max capacity must be at least 1');
        }
        
        // Validate date format
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
            throw new \InvalidArgumentException('Invalid date format. Use YYYY-MM-DD');
        }
        
        // Calculate end_date if not provided
        if (empty($data['end_date'])) {
            $trip = $this->tripRepository->find((int) $data['trip_id']);
            $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
            $data['end_date'] = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
        }
        
        $data['start_date'] = $startDate;
        $data['date'] = $startDate; // Keep for backward compatibility
        
        // Check if departure already exists for this trip and start_date
        $existing = $this->repository->findByTripIdAndStartDate(
            (int) $data['trip_id'],
            $startDate,
            $data['time'] ?? null
        );
        
        if ($existing) {
            throw new \InvalidArgumentException('A departure already exists for this trip and date');
        }
        
        // Set defaults
        $data['source'] = $data['source'] ?? 'manual'; // Admin-created departures are 'manual'
        $data['booked_count'] = $data['booked_count'] ?? 0;
        
        // Create departure
        $id = $this->repository->create($data);
        
        // Trigger hook to sync capacity from availability
        do_action('yatra_departure_saved', $id);
        
        // Recalculate status
        $departure = $this->repository->findModel($id);
        if ($departure) {
            $this->repository->update($id, ['status' => $departure->calculateStatus()]);
        }
        
        return $id;
    }

    /**
     * Update a departure (for admin editing)
     */
    public function update(int $id, array $data): bool
    {
        $departure = $this->repository->findModel($id);
        
        if (!$departure) {
            throw new \InvalidArgumentException('Departure not found');
        }
        
        // Handle start_date and end_date
        $startDate = $data['start_date'] ?? $data['date'] ?? null;
        
        // Validate date format if provided
        if ($startDate && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
            throw new \InvalidArgumentException('Invalid date format. Use YYYY-MM-DD');
        }
        
        // Calculate end_date if start_date changed but end_date not provided
        if ($startDate && $startDate !== ($departure->start_date ?: $departure->date)) {
            if (empty($data['end_date'])) {
                $trip = $this->tripRepository->find($departure->trip_id);
                $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
                $data['end_date'] = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
            }
            $data['start_date'] = $startDate;
            $data['date'] = $startDate; // Keep in sync
        }
        
        // Check for duplicate if start_date is being changed
        if ($startDate && $startDate !== ($departure->start_date ?: $departure->date)) {
            $existing = $this->repository->findByTripIdAndStartDate(
                $departure->trip_id,
                $startDate,
                $data['time'] ?? $departure->time
            );
            
            if ($existing && $existing->id !== $id) {
                throw new \InvalidArgumentException('A departure already exists for this trip and date');
            }
        }
        
        // Mark as manually edited by admin
        if (!isset($data['source'])) {
            $data['source'] = 'manual'; // Admin edits mark as manual
        }
        
        // Update departure
        $result = $this->repository->update($id, $data);
        
        // Trigger hook to sync capacity from availability
        if ($result) {
            do_action('yatra_departure_saved', $id);
        }

        // If status is being explicitly set to 'trash' (admin trash feature),
        // skip automatic status recalculation so the trashed state is preserved.
        if (isset($data['status']) && $data['status'] === 'trash') {
            return $result;
        }
        
        // Recalculate status for all other updates
        $departure = $this->repository->findModel($id);
        if ($departure) {
            $this->repository->update($id, ['status' => $departure->calculateStatus()]);
        }
        
        return $result;
    }

    /**
     * Delete a departure.
     *
     * Allowed once the departure no longer has any booking attached, whether it
     * was created automatically or by hand — an operator who adds a departure by
     * mistake must be able to remove it again.
     */
    public function delete(int $id): bool
    {
        $departure = $this->repository->findModel($id);

        if (!$departure) {
            throw new \InvalidArgumentException('Departure not found');
        }

        // Bookings themselves are the source of truth here, not `booked_count`:
        // that counter can drift upwards (a cancelled or expired booking does not
        // always decrement it), which would otherwise leave a departure
        // permanently undeletable long after its last booking went away.
        //
        // This is the ONLY thing standing between a departure and deletion. The
        // previous guard also required source === 'recurring_generated', a value
        // this plugin never writes (departures are `booking_created` or `manual`,
        // see Departure::$source), so no departure was ever deletable at all.
        if (!empty($this->bookingRepository->findByDepartureId($id))) {
            throw new \InvalidArgumentException(
                __('Cannot delete departure: it still has bookings attached.', 'yatra')
            );
        }

        return $this->repository->delete($id);
    }

    /**
     * Increment booked count (when booking is created).
     *
     * @param bool $force When true, bypasses the capacity guard so the
     *   increment lands even if it would exceed `max_capacity`. Use
     *   only for after-the-fact reconciliation paths — most commonly
     *   external-channel bookings (Viator / GetYourGuide / any OTA
     *   webhook) where the seat has ALREADY been sold on the OTA
     *   side. Refusing to record the increment would hide the oversell
     *   from the operator and break reconciliation. Direct-checkout
     *   callers should leave this false to keep overbooking protection.
     */
    public function incrementBookedCount(int $id, int $amount = 1, bool $force = false): bool
    {
        $departure = $this->repository->findModel($id);

        if (!$departure) {
            throw new \InvalidArgumentException('Departure not found');
        }

        // Capacity pre-check is the same guard as before — but only
        // for non-forced callers. Forced callers (OTA ingest) skip it
        // entirely and rely on the repository to write unconditionally.
        if (!$force) {
            $currentBooked = (int) ($departure->booked_count ?? 0);
            $maxCapacity = $departure->max_capacity !== null ? (int) $departure->max_capacity : 0;
            if ($maxCapacity > 0 && ($currentBooked + $amount > $maxCapacity)) {
                // Do not throw; just prevent exceeding capacity.
                return false;
            }
        }

        $ok = $this->repository->incrementBookedCount($id, $amount, $force);
        if ($ok) {
            $dep = $this->repository->findModel($id);
            if ($dep) {
                $st = $dep->calculateStatus();
                if ($st !== $dep->status) {
                    $this->repository->update($id, ['status' => $st]);
                }
            }
        }

        return $ok;
    }

    /**
     * Decrement booked count (when booking is cancelled)
     */
    public function decrementBookedCount(int $id, int $amount = 1): bool
    {
        $departure = $this->repository->findModel($id);
        
        if (!$departure) {
            throw new \InvalidArgumentException('Departure not found');
        }
        
        $ok = $this->repository->decrementBookedCount($id, $amount);
        if ($ok) {
            $dep = $this->repository->findModel($id);
            if ($dep) {
                $st = $dep->calculateStatus();
                if ($st !== $dep->status) {
                    $this->repository->update($id, ['status' => $st]);
                }
            }
        }

        return $ok;
    }

    /**
     * Get all departures across all trips
     */
    public function getAllDepartures(array $filters = []): array
    {
        return $this->repository->findAll($filters);
    }

    /**
     * Get departures by trip ID
     */
    public function getByTripId(int $tripId, array $filters = []): array
    {
        return $this->repository->findByTripId($tripId, $filters);
    }

    /**
     * Get past departures by trip ID
     */
    public function getPastByTripId(int $tripId, array $filters = []): array
    {
        return $this->repository->findPastByTripId($tripId, $filters);
    }

    /**
     * Get upcoming departures by trip ID
     */
    public function getUpcomingByTripId(int $tripId, array $filters = []): array
    {
        return $this->repository->findUpcomingByTripId($tripId, $filters);
    }

    /**
     * Get available dates for frontend
     * Combines manual departures and dynamically generated recurring rule dates
     * 
     * @param int $tripId Trip ID
     * @param string $fromDate Start date (default: today)
     * @param string $toDate End date (default: +12 months)
     * @return array Available dates with pricing and capacity info
     */
    public function getAvailableDates(int $tripId, ?string $fromDate = null, ?string $toDate = null): array
    {
        $fromDate = $fromDate ?? date('Y-m-d');
        $toDate = $toDate ?? date('Y-m-d', strtotime('+12 months'));
        
        // Get all manual departures
        $manualDepartures = $this->repository->findByTripId($tripId, [
            'date_from' => $fromDate,
            'date_to' => $toDate,
            'include_past' => false,
        ]);
        
        // Get recurring rule service
        $ruleRepository = new \Yatra\Repositories\RecurringRuleRepository();
        $ruleService = new RecurringRuleService($ruleRepository, $this->repository);
        
        // Generate dates from recurring rules
        $recurringDates = $ruleService->generateDatesForTrip($tripId, $fromDate, $toDate);
        
        // Combine and format
        $availableDates = [];
        
        // Add manual departures
        foreach ($manualDepartures as $departure) {
            if ($departure->isAvailable()) {
                $availableDates[$departure->date] = [
                    'id' => $departure->id,
                    'date' => $departure->date,
                    'time' => $departure->time,
                    'max_capacity' => $departure->max_capacity,
                    'available_capacity' => $departure->max_capacity - $departure->booked_count,
                    'booked_count' => $departure->booked_count,
                    'status' => $departure->status,
                    'source' => $departure->source,
                    'price_override' => $departure->price_override,
                    'price_by_traveler_type' => $departure->price_by_traveler_type,
                    'is_full' => $departure->booked_count >= $departure->max_capacity,
                ];
            }
        }
        
        // Add recurring rule dates (only if no manual departure exists)
        foreach ($recurringDates as $dateInfo) {
            if (!isset($availableDates[$dateInfo['date']])) {
                $availableDates[$dateInfo['date']] = [
                    'id' => null,
                    'date' => $dateInfo['date'],
                    'time' => null,
                    'max_capacity' => $dateInfo['max_capacity'],
                    'available_capacity' => $dateInfo['max_capacity'],
                    'booked_count' => 0,
                    'status' => 'upcoming',
                    'source' => 'recurring_rule',
                    'price_override' => $dateInfo['base_price'],
                    'price_by_traveler_type' => $dateInfo['pricing_by_traveler_type'],
                    'is_full' => false,
                    'rule_id' => $dateInfo['rule_id'],
                ];
            }
        }
        
        // Sort by date
        ksort($availableDates);
        
        return array_values($availableDates);
    }

    /**
     * Recalculate all departure statuses (for cron job)
     */
    public function recalculateAllStatuses(): int
    {
        return $this->repository->recalculateAllStatuses();
    }

    /**
     * Find or create a departure for a booking
     * If departure doesn't exist, creates it automatically
     * 
     * @param int $tripId Trip ID
     * @param string $startDate Start date (YYYY-MM-DD)
     * @param string $endDate End date (YYYY-MM-DD)
     * @param int $travelersCount Number of travelers in the booking
     * @param int|null $defaultMaxCapacity Default max capacity if creating new departure (null = unlimited)
     * @param string|null $time Time in HH:MM:SS format (optional)
     * @return Departure The departure (existing or newly created)
     * @throws \Exception
     */
    public function findOrCreateForBooking(int $tripId, string $startDate, string $endDate, int $travelersCount = 0, ?int $defaultMaxCapacity = null, ?string $time = null): Departure
    {
        // Get capacity based on priority
        $maxCapacity = $this->capacityService->getCapacityForDate($tripId, $startDate);
        
        // If no capacity found from availability or rules, use the provided default
        if ($maxCapacity <= 0 && $defaultMaxCapacity !== null) {
            $maxCapacity = $defaultMaxCapacity;
        }
        // Validate date format
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
            throw new \InvalidArgumentException('Invalid start date format. Use YYYY-MM-DD');
        }
        if (!empty($endDate) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
            throw new \InvalidArgumentException('Invalid end date format. Use YYYY-MM-DD');
        }

        // Try to find existing departure by start_date and time
        $departure = $this->repository->findByTripIdAndStartDate($tripId, $startDate, $time);

        if ($departure) {
            // Departure exists, sync capacity from availability before returning
            if ($maxCapacity > 0 && $departure->max_capacity !== $maxCapacity) {
                $this->repository->update($departure->id, ['max_capacity' => $maxCapacity]);
                $departure->max_capacity = $maxCapacity;
            }
            return $departure;
        }

        // Departure doesn't exist, create it
        $trip = $this->tripRepository->find($tripId);

        // Calculate end_date if not provided
        if (empty($endDate)) {
            $durationDays = $trip ? ($trip->duration_days ?? 1) : 1;
            $endDate = date('Y-m-d', strtotime($startDate . ' + ' . ($durationDays - 1) . ' days'));
        }

        // Keep capacity from availability/rules/trip (resolved above). Only use caller default if still unknown.
        if ($maxCapacity <= 0 && $defaultMaxCapacity !== null && $defaultMaxCapacity > 0) {
            $maxCapacity = $defaultMaxCapacity;
        }
        if ($maxCapacity <= 0) {
            $maxCapacity = $trip ? max(1, (int) ($trip->max_travelers ?? $trip->max_travellers ?? 1)) : 1;
        }

        // Trip already loaded for end_date / capacity fallbacks
        $defaultTime = $time;
        
        // If no time provided, try to get from trip settings or use default
        if (!$defaultTime && $trip) {
            // Check if trip has a default departure time
            $defaultTime = $trip->departure_time ?? '09:00'; // Default to 9:00 AM
        }
        
        $departureData = [
            'trip_id' => $tripId,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'date' => $startDate, // Keep for backward compatibility
            'time' => $defaultTime, // Add time (default or provided)
            'max_capacity' => $maxCapacity,
            'booked_count' => 0, // Will be incremented after creation
            'total_revenue' => 0.00, // Initialize revenue
            'source' => 'booking_created', // Created from booking
            'status' => 'upcoming', // Will be recalculated
        ];

        $departureId = $this->repository->create($departureData);
        $departure = $this->repository->findModel($departureId);

        if (!$departure) {
            throw new \RuntimeException('Failed to create departure');
        }

        return $departure;
    }

    /**
     * Link a booking to a departure
     * 
     * @param int $bookingId Booking ID
     * @param int $departureId Departure ID
     * @return bool Success
     */
    public function linkBookingToDeparture(int $bookingId, int $departureId): bool
    {
        $result = $this->bookingDepartureRepository->link($bookingId, $departureId);
        
        if ($result) {
            $this->recalculateDepartureRevenue($departureId);
            $departure = $this->repository->findModel($departureId);
            if ($departure) {
                $newStatus = $departure->calculateStatus();
                if ($newStatus !== $departure->status) {
                    $this->repository->update($departureId, ['status' => $newStatus]);
                }
            }
        }
        
        return $result;
    }

    /**
     * Unlink a booking from a departure
     * If departure has no more bookings, mark it as cancelled
     * 
     * @param int $bookingId Booking ID
     * @param int|null $departureId Optional departure ID
     * @return bool Success
     */
    public function unlinkBookingFromDeparture(int $bookingId, ?int $departureId = null): bool
    {
        $booking = $this->bookingRepository->find($bookingId);
        $travelersCount = $booking ? max(0, (int) ($booking->travelers_count ?? 0)) : 0;

        // Get departure ID if not provided
        if ($departureId === null) {
            $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
            if ($departureId === null) {
                return true; // No link exists
            }
        }

        // Unlink booking
        $result = $this->bookingDepartureRepository->unlink($bookingId, $departureId);
        
        if (!$result) {
            return false;
        }

        if ($travelersCount > 0) {
            $this->decrementBookedCount($departureId, $travelersCount);
        }

        // Recalculate total revenue for the departure
        $this->recalculateDepartureRevenue($departureId);

        // Check if departure has any more bookings
        $bookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($departureId);
        
        if ($bookingCount === 0) {
            // Mark departure as cancelled with note
            $this->cancelDeparture($departureId, 'Cancelled - All bookings removed');
        } else {
            // Recalculate status
            $departure = $this->repository->findModel($departureId);
            if ($departure) {
                $this->repository->update($departureId, ['status' => $departure->calculateStatus()]);
            }
        }

        return true;
    }

    /**
     * Cancel a departure (mark as cancelled with note, never delete)
     * 
     * @param int $departureId Departure ID
     * @param string $note Cancellation note
     * @return bool Success
     */
    public function cancelDeparture(int $departureId, string $note): bool
    {
        $departure = $this->repository->findModel($departureId);
        
        if (!$departure) {
            return false;
        }

        // Update status and notes
        return $this->repository->update($departureId, [
            'status' => 'cancelled',
            'notes' => $note,
        ]);
    }

    /**
     * Handle booking date change
     * Creates new departure and cancels old one
     * 
     * @param int $bookingId Booking ID
     * @param string $newStartDate New start date
     * @param string $newEndDate New end date
     * @return array {success: bool, new_departure_id: int, old_departure_id: int|null}
     */
    public function handleBookingDateChange(int $bookingId, string $newStartDate, string $newEndDate): array
    {
        // Get booking to find trip_id
        $booking = $this->bookingRepository->find($bookingId);
        if (!$booking) {
            throw new \InvalidArgumentException('Booking not found');
        }

        $tripId = (int) $booking->trip_id;
        $oldDepartureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);

        // Get trip for max capacity
        $trip = $this->tripRepository->find($tripId);
        // Get max capacity from trip's max_travelers, or use default
        $maxCapacity = null;
        if ($trip && !empty($trip->max_travelers)) {
            $maxCapacity = (int) $trip->max_travelers;
        }

        // Find or create new departure
        $newDeparture = $this->findOrCreateForBooking($tripId, $newStartDate, $newEndDate, 0, $maxCapacity);

        // Link booking to new departure
        $this->bookingDepartureRepository->updateDepartureForBooking($bookingId, $newDeparture->id);

        // Increment booked count for new departure
        $travelersCount = (int) ($booking->travelers_count ?? 0);
        $this->incrementBookedCount($newDeparture->id, $travelersCount);

        // Handle old departure (link was moved in updateDepartureForBooking; adjust counts explicitly)
        if ($oldDepartureId) {
            if ($travelersCount > 0) {
                $this->decrementBookedCount($oldDepartureId, $travelersCount);
            }
            $oldBookingCount = $this->bookingDepartureRepository->countBookingsForDeparture($oldDepartureId);
            if ($oldBookingCount === 0) {
                $this->cancelDeparture($oldDepartureId, "Cancelled - Booking date changed (Booking ID: {$bookingId})");
            }
        }

        return [
            'success' => true,
            'new_departure_id' => $newDeparture->id,
            'old_departure_id' => $oldDepartureId,
        ];
    }

    /**
     * Get all bookings for a departure
     * 
     * @param int $departureId Departure ID
     * @return array Array of booking objects
     */
    public function getBookingsForDeparture(int $departureId): array
    {
        // Use repository helper to get booking IDs for this departure
        $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
        
        if (empty($bookingIds)) {
            return [];
        }

        $bookings = [];
        foreach ($bookingIds as $bookingId) {
            $bookingId = (int) $bookingId;
            if ($bookingId <= 0) {
                continue;
            }
            $booking = $this->bookingRepository->find($bookingId);
            if ($booking) {
                $bookings[] = $booking;
            }
        }

        return $bookings;
    }

    /**
     * Get departure for a booking
     * 
     * @param int $bookingId Booking ID
     * @return Departure|null Departure or null
     */
    public function getDepartureForBooking(int $bookingId): ?Departure
    {
        $departureId = $this->bookingDepartureRepository->getDepartureIdForBooking($bookingId);
        
        if ($departureId === null) {
            return null;
        }

        return $this->repository->findModel($departureId);
    }

    /**
     * Check if a date matches a recurring rule
     * 
     * @param string $date Date to check (YYYY-MM-DD)
     * @param object $rule Recurring rule object
     * @return bool True if date matches the rule
     */
    private function dateMatchesRecurringRule(string $date, object $rule): bool
    {
        // Check date range
        if (!empty($rule->start_date) && $date < $rule->start_date) {
            return false;
        }
        if (!empty($rule->end_date) && $date > $rule->end_date) {
            return false;
        }

        $dayOfWeek = (int) date('w', strtotime($date)); // 0 = Sunday, 6 = Saturday
        $recurrenceType = $rule->recurrence_type ?? 'daily';
        $weekdays = is_string($rule->weekdays) ? json_decode($rule->weekdays, true) : ($rule->weekdays ?? []);

        switch ($recurrenceType) {
            case 'daily':
                return true;
            
            case 'weekly':
                return in_array($dayOfWeek, $weekdays, true);
            
            case 'monthly':
                // Check if it's the same day of month
                $ruleDay = !empty($rule->start_date) ? (int) date('d', strtotime($rule->start_date)) : null;
                $checkDay = (int) date('d', strtotime($date));
                return $ruleDay === null || $ruleDay === $checkDay;
            
            case 'custom_days':
                return in_array($dayOfWeek, $weekdays, true);
            
            default:
                return false;
        }
    }

    /**
     * Get the departure repository
     * 
     * @return \Yatra\Repositories\DepartureRepository
     */
    public function getRepository(): \Yatra\Repositories\DepartureRepository
    {
        return $this->repository;
    }
    
    /**
     * Recalculate total revenue for a departure from all linked bookings
     * 
     * @param int $departureId Departure ID
     * @return float Total revenue
     */
    public function recalculateDepartureRevenue(int $departureId): float
    {
        // Use repository helper to fetch booking IDs
        $bookingIds = $this->bookingDepartureRepository->getBookingIdsForDeparture($departureId);
        
        if (empty($bookingIds)) {
            $this->repository->update($departureId, ['total_revenue' => 0.00]);
            return 0.00;
        }

        $totalRevenue = 0.00;
        
        foreach ($bookingIds as $bookingId) {
            $bookingId = (int) $bookingId;
            if ($bookingId <= 0) {
                continue;
            }
            $booking = $this->bookingRepository->find($bookingId);
            if ($booking) {
                // Only count confirmed/pending bookings, exclude cancelled/refunded
                if (!in_array($booking->status ?? '', ['cancelled', 'refunded', 'failed'], true)) {
                    // If total_amount is not set, try to calculate from price * travelers
                    if (!empty($booking->total_amount)) {
                        $totalRevenue += (float) $booking->total_amount;
                    } else if (!empty($booking->price) && !empty($booking->traveler_count)) {
                        $totalRevenue += ((float) $booking->price * (int) $booking->traveler_count);
                    } else if (!empty($booking->price)) {
                        // Fallback to just price if traveler count not available
                        $totalRevenue += (float) $booking->price;
                    }
                }
            }
        }

        // Update departure with calculated revenue
        $this->repository->update($departureId, ['total_revenue' => $totalRevenue]);
        
        return $totalRevenue;
    }
}


```
