| @@ -78,12 +78,25 @@ | ||
| 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 | + // 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. | |
| 82 | 96 | $departuresList = $this->request('GET', '/yatra/v1/departures', [ |
| 83 | 97 | 'date_from' => $dateFrom, |
| 84 | - 'date_to' => $dateTo, | |
| 85 | - 'include_past' => 'false', | |
| 98 | + 'include_past' => 'true', | |
| 86 | 99 | ]); |
| 87 | 100 | |
| 88 | 101 | $bookings = isset($bookingsList['data']) && is_array($bookingsList['data']) |
| 89 | 102 | ? $bookingsList['data'] |
| @@ -371,9 +384,9 @@ | ||
| 371 | 384 | $tripTitle = $d['trip']['title'] ?? ($d['trip_title'] ?? ''); |
| 372 | 385 | if ($tripTitle === '') { |
| 373 | 386 | continue; |
| 374 | 387 | } |
| 375 | - $cap = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 388 | + $cap = $this->departureAvailabilityCapacity($d); | |
| 376 | 389 | $bkd = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 377 | 390 | if (!isset($occupancyByTripTitle[$tripTitle])) { |
| 378 | 391 | $occupancyByTripTitle[$tripTitle] = ['booked' => 0, 'capacity' => 0]; |
| 379 | 392 | } |
| @@ -437,8 +450,9 @@ | ||
| 437 | 450 | // ------------------------------------------------------------------ |
| 438 | 451 | $upcomingDepartures = 0; |
| 439 | 452 | $totalCapacity = 0; |
| 440 | 453 | $bookedCapacity = 0; |
| 454 | + $departuresWithBookings = 0; | |
| 441 | 455 | $upcomingTrips = []; |
| 442 | 456 | |
| 443 | 457 | $todayTs = strtotime('today'); |
| 444 | 458 | |
| @@ -450,20 +464,27 @@ | ||
| 450 | 464 | $upcomingTrips[] = [ |
| 451 | 465 | 'trip' => $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra')), |
| 452 | 466 | 'date' => $dateStr, |
| 453 | 467 | 'booked' => (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0), |
| 454 | - 'capacity' => (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0), | |
| 468 | + 'capacity' => $this->departureAvailabilityCapacity($d), | |
| 455 | 469 | ]; |
| 456 | 470 | } |
| 457 | 471 | |
| 458 | - $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 472 | + $capacity = $this->departureAvailabilityCapacity($d); | |
| 459 | 473 | $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 460 | 474 | $totalCapacity += $capacity; |
| 461 | 475 | $bookedCapacity += $booked; |
| 476 | + if ($booked > 0) { | |
| 477 | + $departuresWithBookings++; | |
| 478 | + } | |
| 462 | 479 | } |
| 463 | 480 | |
| 464 | 481 | $occupancyRate = $totalCapacity > 0 ? round(($bookedCapacity / $totalCapacity) * 100.0, 1) : 0.0; |
| 465 | - $averageGroupSize = $upcomingDepartures > 0 ? round($bookedCapacity / $upcomingDepartures, 1) : 0.0; | |
| 482 | + // Average size of an actual booked group: total booked travellers over the | |
| 483 | + // departures that have bookings. Divides over the SAME set the numerator | |
| 484 | + // sums (all in-window departures with bookings) — not the upcoming-only | |
| 485 | + // count, which mismatched the window-wide numerator and inflated the value. | |
| 486 | + $averageGroupSize = $departuresWithBookings > 0 ? round($bookedCapacity / $departuresWithBookings, 1) : 0.0; | |
| 466 | 487 | |
| 467 | 488 | $operationalStats = [ |
| 468 | 489 | 'upcomingDepartures' => $upcomingDepartures, |
| 469 | 490 | 'totalCapacity' => $totalCapacity, |
| @@ -704,9 +725,9 @@ | ||
| 704 | 725 | |
| 705 | 726 | foreach ($departures as $d) { |
| 706 | 727 | $dateStr = $d['start_date'] ?? ($d['date'] ?? null); |
| 707 | 728 | $tripTitle = $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra')); |
| 708 | - $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0); | |
| 729 | + $capacity = $this->departureAvailabilityCapacity($d); | |
| 709 | 730 | $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0); |
| 710 | 731 | $left = $capacity > 0 ? max(0, $capacity - $booked) : 0; |
| 711 | 732 | $status = strtolower((string) ($d['status'] ?? 'upcoming')); |
| 712 | 733 | |
| @@ -944,8 +965,52 @@ | ||
| 944 | 965 | 'refunds' => $refundsSummary, |
| 945 | 966 | 'top_destinations' => $topDestinations, |
| 946 | 967 | ], |
| 947 | 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); | |
| 948 | 1013 | } |
| 949 | 1014 | |
| 950 | 1015 | /** |
| 951 | 1016 | * Helper to call an internal REST endpoint and return decoded data |