# yatra/3.0.14.2/app/Hooks/AvailabilityInventoryHooks.php

Yatra – Travel Booking &amp; Tour Operator Software, version 3.0.14.2. 314 lines.

- Page: https://pluginprobe.com/plugins/yatra/3.0.14.2/code/app/Hooks/AvailabilityInventoryHooks.php
- Raw: https://pluginprobe.com/plugins/yatra/3.0.14.2/raw/app/Hooks/AvailabilityInventoryHooks.php
- Modified: 2026-07-21T11:17:18+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.14.2/code/app/Hooks/AvailabilityInventoryHooks.php#L10-L20`.

```php
<?php

declare(strict_types=1);

namespace Yatra\Hooks;

use Yatra\Database\Tables\BookingsTable;
use Yatra\Database\Tables\TripAvailabilityDatesTable;
use Yatra\Repositories\AvailabilityRepository;
use Yatra\Repositories\BookingDepartureRepository;
use Yatra\Repositories\BookingRepository;
use Yatra\Repositories\DepartureRepository;
use Yatra\Services\CapacityService;
use Yatra\Services\WaitlistPromotionService;

class AvailabilityInventoryHooks
{
    public static function init(): void
    {
        add_action('yatra_booking_created', [self::class, 'onBookingCreated'], 10, 2);
        add_action('yatra_booking_deleted', [self::class, 'onBookingDeleted'], 10, 2);
        add_action('yatra_booking_status_changed', [self::class, 'onBookingStatusChanged'], 10, 3);
        
        // Add hooks to ensure departure capacity sync
        add_action('yatra_availability_updated', [self::class, 'onAvailabilityUpdate'], 10, 1);
        add_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10, 1);
    }

    public static function onBookingCreated(int $bookingId, $booking = null): void
    {
        self::syncForBooking($bookingId, $booking);
    }

    public static function onBookingDeleted(int $bookingId, $booking = null): void
    {
        self::syncForBooking($bookingId, $booking);
    }

    public static function onBookingStatusChanged(int $bookingId, string $oldStatus, string $newStatus): void
    {
        self::syncForBooking($bookingId, null);
    }

    private static function syncForBooking(int $bookingId, $booking = null): void
    {
        try {
            if (!is_object($booking)) {
                $repo = new BookingRepository();
                $booking = $repo->find($bookingId);
            }

            if (!is_object($booking)) {
                return;
            }

            $tripId = (int) ($booking->trip_id ?? 0);
            $availabilityId = isset($booking->availability_id) ? (int) $booking->availability_id : 0;
            $travelDate = (string) ($booking->travel_date ?? '');

            if ($tripId > 0 && $availabilityId <= 0 && $travelDate !== '') {
                // Try to infer availability_id from resolved snapshot stored in booking meta.
                // This is more reliable when a trip has multiple departure times on the same date.
                $metaRaw = $booking->meta ?? null;
                $meta = [];
                if (is_string($metaRaw) && $metaRaw !== '') {
                    $decoded = json_decode($metaRaw, true);
                    $meta = is_array($decoded) ? $decoded : [];
                } elseif (is_array($metaRaw)) {
                    $meta = $metaRaw;
                }

                if (!empty($meta) && isset($meta['resolved_availability']) && is_array($meta['resolved_availability'])) {
                    $ra = $meta['resolved_availability'];

                    $metaAvailabilityId = $ra['availability_id'] ?? null;
                    if (is_numeric($metaAvailabilityId) && (int) $metaAvailabilityId > 0) {
                        $availabilityId = (int) $metaAvailabilityId;
                    } else {
                        $metaDepartureTime = isset($ra['departure_time']) ? (string) $ra['departure_time'] : '';
                        $metaTravelDate = isset($ra['travel_date']) ? (string) $ra['travel_date'] : $travelDate;
                        if ($metaDepartureTime !== '') {
                            $row = (new AvailabilityRepository())->findByTripIdAndDateTime(
                                $tripId,
                                $metaTravelDate,
                                self::normalizeTimeForAvailabilityMatch($metaDepartureTime),
                                true
                            );
                            if ($row && !empty($row->id)) {
                                $availabilityId = (int) $row->id;
                            }
                        }
                    }
                }

                $availabilityId = self::resolveAvailabilityIdForBooking($bookingId, $tripId, $travelDate);
                if ($availabilityId > 0) {
                    (new BookingRepository())->update($bookingId, ['availability_id' => $availabilityId]);
                }
            }

            if ($tripId <= 0 || $availabilityId <= 0) {
                return;
            }

            self::syncAvailabilityId($availabilityId, $tripId);
        } catch (\Throwable $e) {
            return;
        }
    }

    private static function syncAvailabilityId(int $availabilityId, int $tripId): void
    {
        $availabilityRepo = new AvailabilityRepository();
        $availability = $availabilityRepo->find($availabilityId);

        if (!$availability) {
            return;
        }

        $departureTime = isset($availability->departure_time) ? (string) $availability->departure_time : '';

        $bookingRepo = new BookingRepository();
        $bookedCount = $bookingRepo->getTotalTravelersByTripAndAvailability(
            $tripId,
            $availabilityId,
            BookingRepository::getCapacityConsumingBookingStatuses()
        );

        $seatsTotal = (int) ($availability->seats_total ?? 0);
        $seatsReserved = $bookedCount;
        $seatsAvailable = max(0, $seatsTotal - $bookedCount);

        $status = (string) ($availability->status ?? 'available');
        if (!in_array($status, ['blocked', 'closed', 'cancelled'], true)) {
            if ($seatsAvailable === 0) {
                $status = 'sold_out';
            } elseif ($seatsTotal > 0 && $seatsAvailable <= (int) ceil($seatsTotal * 0.2)) {
                $status = 'limited';
            } else {
                $status = 'available';
            }
        }

        // Update availability using repository
        $availabilityRepo->update($availabilityId, [
            'seats_available' => $seatsAvailable,
            'seats_reserved' => $seatsReserved,
            'status' => $status,
        ]);

        self::syncDepartureWithAvailability($tripId, $availability, $bookedCount, $seatsTotal);

        WaitlistPromotionService::processAvailableSlots($tripId, $availabilityId);
    }

    private static function syncDepartureWithAvailability(int $tripId, object $availability, int $bookedCount, int $seatsTotal): void
    {
        $departureRepo = new DepartureRepository();
        $date = (string) ($availability->departure_date ?? '');
        $time = (string) ($availability->departure_time ?? '');
        $normalizedTime = '';
        if ($time !== '') {
            $ts = strtotime($time);
            if ($ts !== false) {
                $normalizedTime = date('H:i:s', $ts);
            }
        }

        if ($date === '') {
            return;
        }

        // Find a matching departure by trip + date (+ time if provided)
        $departure = null;
        $candidateTimes = array_values(array_unique(array_filter([$normalizedTime, $time], function ($t) {
            return $t !== '';
        })));

        foreach ($candidateTimes as $candidateTime) {
            $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, $candidateTime);
            if ($departure) {
                break;
            }
        }
        if (!$departure) {
            $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, null);
        }

        if (!$departure) {
            return;
        }

        // booked_count is sourced from the actual bookings table — it
        // is the truth. Reflect it to the departure row WITHOUT
        // clamping to max_capacity. Clamping previously hid oversells
        // from operators: if direct + OTA bookings legitimately
        // exceeded capacity (always an OTA-side acceptance — Yatra's
        // direct checkout enforces capacity), the departure showed
        // "full" instead of "oversold by N", and operators had no way
        // to spot the situation needing reconciliation. Allowing the
        // overshow makes the problem visible so it can be addressed.
        $updated = [
            'max_capacity' => $seatsTotal,
            'booked_count' => max(0, $bookedCount),
        ];

        $departureRepo->update((int) $departure->id, $updated);
    }

    /**
     * Hook to sync departure capacity when availability is updated
     */
    public static function onAvailabilityUpdate(int $availabilityId): void
    {
        $availabilityRepo = new AvailabilityRepository();
        $availability = $availabilityRepo->find($availabilityId);

        if ($availability) {
            self::syncAvailabilityId($availabilityId, (int) $availability->trip_id);
        }
    }

    /**
     * Hook to ensure departure capacity always matches availability on save
     */
    public static function onDepartureSave(int $departureId): void
    {
        // Prevent infinite loops
        static $processing = [];
        if (isset($processing[$departureId])) {
            return;
        }
        $processing[$departureId] = true;
        
        $departureRepo = new DepartureRepository();
        $departure = $departureRepo->find($departureId);
        
        if ($departure) {
            $date = $departure->start_date ?: $departure->date;
            
            // Get capacity from availability
            $capacityService = new CapacityService();
            $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date, $departure->time ?? null);
            
            if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) {
                // Remove hook temporarily to prevent infinite loop
                remove_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10);
                
                $departureRepo->update($departureId, [
                    'max_capacity' => $correctCapacity
                ]);
                
                // Re-add hook
                add_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10, 1);
            }
        }
        
        unset($processing[$departureId]);
    }

    /**
     * Infer yatra_trip_availability_dates.id when the booking row missed availability_id
     * (common when checkout omits it). Uses booking→departure time to disambiguate multiple slots per day.
     */
    private static function resolveAvailabilityIdForBooking(int $bookingId, int $tripId, string $travelDate): int
    {
        $availabilityRepo = new AvailabilityRepository();
        $bdRepo = new BookingDepartureRepository();
        $depId = $bdRepo->getDepartureIdForBooking($bookingId);

        if ($depId) {
            $depRepo = new DepartureRepository();
            $dep = $depRepo->find($depId);
            if (is_object($dep)) {
                $date = (string) ($dep->start_date ?? $dep->date ?? '');
                if ($date === '') {
                    $date = $travelDate;
                }
                $timeRaw = isset($dep->time) ? trim((string) $dep->time) : '';
                if ($timeRaw !== '' && $timeRaw !== '00:00:00') {
                    $candidates = array_values(array_unique(array_filter([
                        $timeRaw,
                        self::normalizeTimeForAvailabilityMatch($timeRaw),
                    ])));
                    foreach ($candidates as $t) {
                        $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, $t, true);
                        if ($row && !empty($row->id)) {
                            return (int) $row->id;
                        }
                    }
                }
                $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, null, true);
                if ($row && !empty($row->id)) {
                    return (int) $row->id;
                }
            }
        }

        $records = $availabilityRepo->findByTripAndDate($tripId, $travelDate);
        if (count($records) === 1 && !empty($records[0]->id)) {
            return (int) $records[0]->id;
        }

        return 0;
    }

    private static function normalizeTimeForAvailabilityMatch(string $time): string
    {
        $ts = strtotime($time);

        return $ts !== false ? date('H:i:s', $ts) : $time;
    }
}

```
