| @@ -78,15 +78,24 @@ | ||
| 78 | 78 | ]; |
| 79 | 79 | |
| 80 | 80 | $bookingsList = $this->request('GET', '/yatra/v1/bookings', $params); |
| 81 | 81 | $paymentsList = $this->request('GET', '/yatra/v1/payments', $params); |
| 82 | - // The reporting window [$dateFrom, $dateTo] is historical (default: last | |
| 83 | - // 30 days), matching the bookings/payments/revenue stats. Departures must | |
| 84 | - // therefore INCLUDE past departures — otherwise "in the last 30 days" AND | |
| 85 | - // "not past" is an empty set, and every occupancy figure renders as 0%. | |
| 82 | + // Departures are deliberately NOT capped at $dateTo. | |
| 83 | + // | |
| 84 | + // The reporting window [$dateFrom, $dateTo] is historical (default: the | |
| 85 | + // last 30 days) because bookings/payments/revenue are historical. Applying | |
| 86 | + // that same upper bound to departures is wrong: an operator's seats live in | |
| 87 | + // UPCOMING departures, so "ends today" excludes exactly the departures the | |
| 88 | + // occupancy figures are about — a site selling future tours then reports 0% | |
| 89 | + // occupancy while each departure page correctly shows e.g. 1/9 = 11.1%. | |
| 90 | + // | |
| 91 | + // So we take departures from the period start onwards, including past ones | |
| 92 | + // (a past departure in the window still counts) and upcoming ones. Every | |
| 93 | + // departure-derived figure — the Occupancy Rate card, per-trip occupancy, | |
| 94 | + // the occupancy trend, seat utilisation and the departures table — reads | |
| 95 | + // this one set, so they always agree with each other. | |
| 86 | 96 | $departuresList = $this->request('GET', '/yatra/v1/departures', [ |
| 87 | 97 | 'date_from' => $dateFrom, |
| 88 | - 'date_to' => $dateTo, | |
| 89 | 98 | 'include_past' => 'true', |
| 90 | 99 | ]); |
| 91 | 100 | |
| 92 | 101 | $bookings = isset($bookingsList['data']) && is_array($bookingsList['data']) |
| @@ -375,9 +384,9 @@ | ||
| 375 | 384 | $tripTitle = $d['trip']['title'] ?? ($d['trip_title'] ?? ''); |
| 376 | 385 | if ($tripTitle === '') { |
| 377 | 386 | continue; |
| 378 | 387 | } |
| 379 | - $cap = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 388 | + $cap = $this->departureAvailabilityCapacity($d); | |
| 380 | 389 | $bkd = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 381 | 390 | if (!isset($occupancyByTripTitle[$tripTitle])) { |
| 382 | 391 | $occupancyByTripTitle[$tripTitle] = ['booked' => 0, 'capacity' => 0]; |
| 383 | 392 | } |
| @@ -455,13 +464,13 @@ | ||
| 455 | 464 | $upcomingTrips[] = [ |
| 456 | 465 | 'trip' => $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra')), |
| 457 | 466 | 'date' => $dateStr, |
| 458 | 467 | 'booked' => (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0), |
| 459 | - 'capacity' => (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0), | |
| 468 | + 'capacity' => $this->departureAvailabilityCapacity($d), | |
| 460 | 469 | ]; |
| 461 | 470 | } |
| 462 | 471 | |
| 463 | - $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 472 | + $capacity = $this->departureAvailabilityCapacity($d); | |
| 464 | 473 | $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 465 | 474 | $totalCapacity += $capacity; |
| 466 | 475 | $bookedCapacity += $booked; |
| 467 | 476 | if ($booked > 0) { |
| @@ -716,9 +725,9 @@ | ||
| 716 | 725 | |
| 717 | 726 | foreach ($departures as $d) { |
| 718 | 727 | $dateStr = $d['start_date'] ?? ($d['date'] ?? null); |
| 719 | 728 | $tripTitle = $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra')); |
| 720 | - $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 729 | + $capacity = $this->departureAvailabilityCapacity($d); | |
| 721 | 730 | $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 722 | 731 | $left = $capacity > 0 ? max(0, $capacity - $booked) : 0; |
| 723 | 732 | $status = strtolower((string) ($d['status'] ?? 'upcoming')); |
| 724 | 733 | |
| @@ -956,8 +965,52 @@ | ||
| 956 | 965 | 'refunds' => $refundsSummary, |
| 957 | 966 | 'top_destinations' => $topDestinations, |
| 958 | 967 | ], |
| 959 | 968 | ]); |
| 969 | + } | |
| 970 | + | |
| 971 | + /** | |
| 972 | + * Resolve a departure's capacity from the Availability configuration — the | |
| 973 | + * same authoritative source the Departures page and the /departures | |
| 974 | + * endpoint use (Availability date > recurring rule > the trip's | |
| 975 | + * max_travelers). The dashboard previously summed each departure's stored | |
| 976 | + * `max_capacity`, which could be a stale trip-settings value or a legacy | |
| 977 | + * "unlimited" default (e.g. 9999/11111), so its capacity and occupancy | |
| 978 | + * disagreed with the Departures page. Reading the live availability figure | |
| 979 | + * keeps them consistent. | |
| 980 | + * | |
| 981 | + * @param array<string,mixed> $d Departure row (from /departures) | |
| 982 | + */ | |
| 983 | + private function departureAvailabilityCapacity(array $d): int | |
| 984 | + { | |
| 985 | + static $capacityService = null; | |
| 986 | + static $memo = []; | |
| 987 | + if ($capacityService === null && class_exists('\\Yatra\\Services\\CapacityService')) { | |
| 988 | + $capacityService = new \Yatra\Services\CapacityService(); | |
| 989 | + } | |
| 990 | + | |
| 991 | + $tripId = (int) ($d['trip_id'] ?? ($d['trip']['id'] ?? 0)); | |
| 992 | + $date = (string) ($d['start_date'] ?? ($d['date'] ?? '')); | |
| 993 | + $time = isset($d['time']) ? (string) $d['time'] : ''; | |
| 994 | + $stored = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 995 | + | |
| 996 | + // The same departure is read across several reporting loops, so memoise | |
| 997 | + // the resolution per (trip, date, time, stored) to avoid re-querying. | |
| 998 | + $key = $tripId . '|' . $date . '|' . $time . '|' . $stored; | |
| 999 | + if (isset($memo[$key])) { | |
| 1000 | + return $memo[$key]; | |
| 1001 | + } | |
| 1002 | + | |
| 1003 | + if ($capacityService !== null && $tripId > 0 && $date !== '') { | |
| 1004 | + $cap = $capacityService->getCapacityForDate($tripId, $date, $time !== '' ? $time : null); | |
| 1005 | + if ($cap > 0) { | |
| 1006 | + return $memo[$key] = $cap; | |
| 1007 | + } | |
| 1008 | + } | |
| 1009 | + | |
| 1010 | + // No availability/trip capacity resolved — fall back to the stored value, | |
| 1011 | + // but drop the legacy "unlimited" sentinels that would inflate occupancy. | |
| 1012 | + return $memo[$key] = ($stored >= 9999 ? 0 : $stored); | |
| 960 | 1013 | } |
| 961 | 1014 | |
| 962 | 1015 | /** |
| 963 | 1016 | * Helper to call an internal REST endpoint and return decoded data |