PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 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 All 84 releases
← All changes | app/Controllers/TripAvailabilityController.php +157 -14 3.0.6 → 3.0.16 View file →
@@ -197,8 +197,26 @@
197 197 // DEPARTURES ENDPOINTS
198 198 // =========================================================================
199 199
200 200 /**
201 + * Sanitised pagination for the departure list endpoints.
202 + *
203 + * Returns [page, per_page]. per_page is 0 when the caller did not ask for
204 + * pagination, so the list keeps returning every matching row for
205 + * consumers that never sent it (the previous behaviour); page is always
206 + * >= 1. Only when per_page > 0 is a LIMIT / OFFSET window applied.
207 + *
208 + * @return array{0: int, 1: int}
209 + */
210 + private function paginationParams(WP_REST_Request $request): array
211 + {
212 + $perPage = max(0, (int) $request->get_param('per_page'));
213 + $page = max(1, (int) $request->get_param('page'));
214 +
215 + return [$page, $perPage];
216 + }
217 +
218 + /**
201 219 * GET /trips/{trip_id}/departures
202 220 */
203 221 public function get_departures(WP_REST_Request $request): WP_REST_Response
204 222 {
@@ -203,15 +221,21 @@
203 221 public function get_departures(WP_REST_Request $request): WP_REST_Response
204 222 {
205 223 $tripId = (int) $request->get_param('trip_id');
206 224 $status = $request->get_param('status');
225 + $availability = $request->get_param('availability');
226 + $search = $request->get_param('search');
207 227 $source = $request->get_param('source');
208 228 $dateFrom = $request->get_param('date_from');
209 229 $dateTo = $request->get_param('date_to');
210 230 $includePast = $request->get_param('include_past') !== 'false';
211 -
231 +
212 232 $filters = [];
213 233 if ($status) $filters['status'] = $status;
234 + // Capacity is filtered independently of status (see DepartureRepository::applyAvailabilityClause).
235 + if ($availability && in_array($availability, ['available', 'partial', 'full'], true)) $filters['availability'] = $availability;
236 + // Free-text search on date / notes (see DepartureRepository::applySearchClause).
237 + if (is_string($search) && trim($search) !== '') $filters['search'] = trim($search);
214 238 if ($source) $filters['source'] = $source;
215 239 if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom;
216 240 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
217 241 $filters['include_past'] = $includePast;
@@ -216,8 +240,17 @@
216 240 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
217 241 $filters['include_past'] = $includePast;
218 242
219 243 try {
244 + // Server-side pagination, only when the caller asks for it.
245 + [$page, $perPage] = $this->paginationParams($request);
246 + if ($perPage > 0) {
247 + $filters['per_page'] = $perPage;
248 + $filters['page'] = $page;
249 + }
250 + // True total for the SAME filters, independent of the page window —
251 + // count($departures) was the size of the returned page, not the total.
252 + $total = $this->departureService->countByTripId($tripId, $filters);
220 253 $departures = $this->departureService->getByTripId($tripId, $filters);
221 254
222 255 // Get trip information
223 256 $tripRepository = new \Yatra\Repositories\TripRepository();
@@ -236,14 +269,29 @@
236 269 'success' => true,
237 270 'data' => array_map(function ($d) use ($trip, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) {
238 271 // Sync capacity from availability before returning
239 272 $date = $d->start_date ?: $d->date;
240 - $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date);
273 + $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date, $d->time ?? null);
241 274 if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) {
242 275 $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]);
243 276 $d->max_capacity = $correctCapacity;
244 277 }
245 -
278 +
279 + // Promote a departure that has taken place to 'past' so a
280 + // completed departure is never shown with (or hidden behind) a
281 + // stale 'upcoming'/'full' status — the daily cron may not have
282 + // run. Cancelled/trashed departures are left as-is. This keeps
283 + // the status badge and the tab counts date-accurate.
284 + if (!in_array($d->status, ['cancelled', 'trash', 'past'], true)) {
285 + $checkDate = (!empty($d->end_date) && $d->end_date !== '0000-00-00')
286 + ? $d->end_date
287 + : ((!empty($d->start_date) && $d->start_date !== '0000-00-00') ? $d->start_date : $d->date);
288 + if (!empty($checkDate) && $checkDate < date('Y-m-d')) {
289 + $departureRepo->update($d->id, ['status' => 'past']);
290 + $d->status = 'past';
291 + }
292 + }
293 +
246 294 $departureArray = $d->toArray();
247 295
248 296 // Add trip information
249 297 if ($trip) {
@@ -330,9 +378,12 @@
330 378 // Debug: Log time and revenue values
331 379 return $departureArray;
332 380 }, $departures),
333 381 'meta' => [
334 - 'total' => count($departures),
382 + 'total' => $total,
383 + 'page' => $page,
384 + 'per_page' => $perPage > 0 ? $perPage : $total,
385 + 'total_pages' => $perPage > 0 ? max(1, (int) ceil($total / $perPage)) : 1,
335 386 ],
336 387 ]);
337 388 } catch (\Exception $e) {
338 389 return new WP_REST_Response([
@@ -362,9 +413,9 @@
362 413
363 414 // Sync capacity from availability before returning
364 415 $capacityService = new \Yatra\Services\CapacityService();
365 416 $date = $departure->start_date ?: $departure->date;
366 - $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date);
417 + $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date, $departure->time ?? null);
367 418 if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) {
368 419 $repo->update($departure->id, ['max_capacity' => $correctCapacity]);
369 420 $departure->max_capacity = $correctCapacity;
370 421 }
@@ -590,15 +641,21 @@
590 641 */
591 642 public function get_all_departures(WP_REST_Request $request): WP_REST_Response
592 643 {
593 644 $status = $request->get_param('status');
645 + $availability = $request->get_param('availability');
646 + $search = $request->get_param('search');
594 647 $source = $request->get_param('source');
595 648 $dateFrom = $request->get_param('date_from');
596 649 $dateTo = $request->get_param('date_to');
597 650 $includePast = $request->get_param('include_past') !== 'false';
598 -
651 +
599 652 $filters = [];
600 653 if ($status) $filters['status'] = $status;
654 + // Capacity is filtered independently of status (see DepartureRepository::applyAvailabilityClause).
655 + if ($availability && in_array($availability, ['available', 'partial', 'full'], true)) $filters['availability'] = $availability;
656 + // Free-text search on date / notes (see DepartureRepository::applySearchClause).
657 + if (is_string($search) && trim($search) !== '') $filters['search'] = trim($search);
601 658 if ($source) $filters['source'] = $source;
602 659 if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom;
603 660 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
604 661 $filters['include_past'] = $includePast;
@@ -603,8 +660,17 @@
603 660 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
604 661 $filters['include_past'] = $includePast;
605 662
606 663 try {
664 + // Server-side pagination, only when the caller asks for it.
665 + [$page, $perPage] = $this->paginationParams($request);
666 + if ($perPage > 0) {
667 + $filters['per_page'] = $perPage;
668 + $filters['page'] = $page;
669 + }
670 + // True total for the SAME filters, independent of the page window —
671 + // count($processed) was the size of the returned page, not the total.
672 + $total = $this->departureService->countAllDepartures($filters);
607 673 // Get all departures (no trip filter)
608 674 $departures = $this->departureService->getAllDepartures($filters);
609 675
610 676 // Get repository for additional data
@@ -620,14 +686,48 @@
620 686 // Process each departure to add related data
621 687 $processed = array_map(function ($d) use ($tripRepository, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) {
622 688 // Sync capacity from availability before returning
623 689 $date = $d->start_date ?: $d->date;
624 - $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date);
625 - if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) {
626 - $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]);
627 - $d->max_capacity = $correctCapacity;
690 + $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date, $d->time ?? null);
691 + if ($correctCapacity > 0) {
692 + if ((int) $d->max_capacity !== $correctCapacity) {
693 + $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]);
694 + $d->max_capacity = $correctCapacity;
695 + }
696 + } elseif ((int) $d->max_capacity >= 9999) {
697 + // Normalise a legacy "unlimited"/junk capacity sentinel (e.g.
698 + // 9999/11111) to the canonical unlimited value 0, so it isn't
699 + // shown as a huge literal number here while the dashboard renders
700 + // >= 9999 as 0 — the two disagreeing on capacity and occupancy.
701 + //
702 + // Deliberately heal to 0 (NOT the trip's max_travelers): 0 means
703 + // "unlimited" to both the capacity guard (incrementBookedCount)
704 + // and Departure::calculateStatus(), so healing can never flip an
705 + // already (over-)booked departure to 'full' — which capping to a
706 + // smaller number would, and 'full' departures drop out of the
707 + // dashboard's upcoming view. This keeps the sentinel's original
708 + // "unlimited" meaning while making both surfaces agree.
709 + if ((int) $d->max_capacity !== 0) {
710 + $departureRepo->update($d->id, ['max_capacity' => 0]);
711 + $d->max_capacity = 0;
712 + }
628 713 }
629 -
714 +
715 + // Promote a departure that has taken place to 'past' so a completed
716 + // departure is never shown with (or hidden behind) a stale
717 + // 'upcoming'/'full' status — the daily cron may not have run.
718 + // Cancelled/trashed departures are left as-is. Keeps the status
719 + // badge and the dashboard/tab counts date-accurate.
720 + if (!in_array($d->status, ['cancelled', 'trash', 'past'], true)) {
721 + $checkDate = (!empty($d->end_date) && $d->end_date !== '0000-00-00')
722 + ? $d->end_date
723 + : ((!empty($d->start_date) && $d->start_date !== '0000-00-00') ? $d->start_date : $d->date);
724 + if (!empty($checkDate) && $checkDate < date('Y-m-d')) {
725 + $departureRepo->update($d->id, ['status' => 'past']);
726 + $d->status = 'past';
727 + }
728 + }
729 +
630 730 $departureArray = $d->toArray();
631 731
632 732 // Add trip information
633 733 $trip = $tripRepository->find($d->trip_id);
@@ -714,9 +814,12 @@
714 814 return new WP_REST_Response([
715 815 'success' => true,
716 816 'data' => $processed,
717 817 'meta' => [
718 - 'total' => count($processed),
818 + 'total' => $total,
819 + 'page' => $page,
820 + 'per_page' => $perPage > 0 ? $perPage : $total,
821 + 'total_pages' => $perPage > 0 ? max(1, (int) ceil($total / $perPage)) : 1,
719 822 ],
720 823 ]);
721 824 } catch (\Exception $e) {
722 825 return new WP_REST_Response([
@@ -757,13 +860,53 @@
757 860 public function get_available_dates(WP_REST_Request $request): WP_REST_Response
758 861 {
759 862 $tripId = (int) $request->get_param('trip_id');
760 863 $fromDate = $request->get_param('from_date') ?: date('Y-m-d');
761 - $toDate = $request->get_param('to_date') ?: date('Y-m-d', strtotime('+12 months'));
864 + // An explicit to_date always wins; only the default follows the
865 + // configurable booking horizon (12 months unless changed). The default
866 + // is counted from TODAY — not from from_date — exactly as before, so a
867 + // client that sends only from_date gets the same window it always did.
868 + $toDate = $request->get_param('to_date') ?: yatra_get_availability_horizon_date();
762 869
763 870 try {
764 871 $dates = $this->departureService->getAvailableDates($tripId, $fromDate, $toDate);
765 -
872 +
873 + // Attach the departure times each date actually runs. The list above is
874 + // keyed by date and reports `time => null` for rule-generated dates, so a
875 + // trip running several departures a day looked like a single slot — and
876 + // an operator booking it from the admin had no way to say which departure
877 + // the booking was for. Capacity is tracked per departure, so such a
878 + // booking reserved no seats at all.
879 + //
880 + // Added as an extra field rather than by changing the row shape, so every
881 + // existing consumer of this endpoint is unaffected.
882 + $timesByDate = [];
883 + try {
884 + $resolver = new \Yatra\Services\AvailabilityResolutionService();
885 + foreach ($resolver->getAllAvailabilityDates($tripId, $fromDate, $toDate) as $slot) {
886 + $slotDate = (string) ($slot->departure_date ?? $slot->date ?? '');
887 + $slotTime = trim((string) ($slot->departure_time ?? ''));
888 + if ($slotDate === '' || $slotTime === '') {
889 + continue;
890 + }
891 + $timesByDate[$slotDate][$slotTime] = true;
892 + }
893 + } catch (\Throwable $e) {
894 + $timesByDate = [];
895 + }
896 +
897 + foreach ($dates as $key => $row) {
898 + $rowDate = is_array($row) ? (string) ($row['date'] ?? '') : (string) ($row->date ?? '');
899 + $times = isset($timesByDate[$rowDate]) ? array_keys($timesByDate[$rowDate]) : [];
900 + sort($times);
901 +
902 + if (is_array($row)) {
903 + $dates[$key]['departure_times'] = $times;
904 + } elseif (is_object($row)) {
905 + $row->departure_times = $times;
906 + }
907 + }
908 +
766 909 return new WP_REST_Response([
767 910 'success' => true,
768 911 'data' => $dates,
769 912 ]);