PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 2.0.11 All 82 releases
← All changes | app/Controllers/ReportsController.php +72 -7 3.0.6trunk View file →
@@ -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