← All changes
|
app/Controllers/TripAvailabilityController.php
+223
-36
3.0.2.8
→
3.0.16
View file →
| @@ -36,61 +36,66 @@ | ||
| 36 | 36 | public function register_routes(): void |
| 37 | 37 | { |
| 38 | 38 | $namespace = 'yatra/v1'; |
| 39 | 39 | |
| 40 | - // All departures endpoint (without trip ID) | |
| 40 | + // All departures endpoint (without trip ID) — view cap. | |
| 41 | 41 | register_rest_route($namespace, '/departures', [ |
| 42 | 42 | [ |
| 43 | 43 | 'methods' => \WP_REST_Server::READABLE, |
| 44 | 44 | 'callback' => [$this, 'get_all_departures'], |
| 45 | - 'permission_callback' => [$this, 'check_permission'], | |
| 45 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 46 | 46 | ], |
| 47 | 47 | ]); |
| 48 | - | |
| 48 | + | |
| 49 | 49 | $base = 'trips/(?P<trip_id>[\d]+)/departures'; |
| 50 | 50 | |
| 51 | - // Departures endpoints | |
| 51 | + // Departures list + create — view cap for read, manage cap | |
| 52 | + // for create (a departure is a scheduled trip instance, not | |
| 53 | + // trip content edit). | |
| 52 | 54 | register_rest_route($namespace, '/' . $base, [ |
| 53 | 55 | [ |
| 54 | 56 | 'methods' => \WP_REST_Server::READABLE, |
| 55 | 57 | 'callback' => [$this, 'get_departures'], |
| 56 | - 'permission_callback' => [$this, 'check_permission'], | |
| 58 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 57 | 59 | ], |
| 58 | 60 | [ |
| 59 | 61 | 'methods' => \WP_REST_Server::CREATABLE, |
| 60 | 62 | 'callback' => [$this, 'create_departure'], |
| 61 | - 'permission_callback' => [$this, 'check_permission'], | |
| 63 | + 'permission_callback' => [$this, 'check_manage_permission'], | |
| 62 | 64 | ], |
| 63 | 65 | ]); |
| 64 | 66 | |
| 67 | + // Single departure — view / update / delete. Update is a | |
| 68 | + // manage operation; DELETE is the cancellation cap because | |
| 69 | + // dropping a departure typically means cancelling it. | |
| 65 | 70 | register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [ |
| 66 | 71 | [ |
| 67 | 72 | 'methods' => \WP_REST_Server::READABLE, |
| 68 | 73 | 'callback' => [$this, 'get_departure'], |
| 69 | - 'permission_callback' => [$this, 'check_permission'], | |
| 74 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 70 | 75 | ], |
| 71 | 76 | [ |
| 72 | 77 | 'methods' => \WP_REST_Server::EDITABLE, |
| 73 | 78 | 'callback' => [$this, 'update_departure'], |
| 74 | - 'permission_callback' => [$this, 'check_permission'], | |
| 79 | + 'permission_callback' => [$this, 'check_manage_permission'], | |
| 75 | 80 | ], |
| 76 | 81 | [ |
| 77 | 82 | 'methods' => \WP_REST_Server::DELETABLE, |
| 78 | 83 | 'callback' => [$this, 'delete_departure'], |
| 79 | - 'permission_callback' => [$this, 'check_permission'], | |
| 84 | + 'permission_callback' => [$this, 'check_cancel_permission'], | |
| 80 | 85 | ], |
| 81 | 86 | ]); |
| 82 | 87 | |
| 83 | - // Past departures endpoint | |
| 88 | + // Past departures endpoint — view cap. | |
| 84 | 89 | register_rest_route($namespace, '/' . $base . '/past', [ |
| 85 | 90 | [ |
| 86 | 91 | 'methods' => \WP_REST_Server::READABLE, |
| 87 | 92 | 'callback' => [$this, 'get_past_departures'], |
| 88 | - 'permission_callback' => [$this, 'check_permission'], | |
| 93 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 89 | 94 | ], |
| 90 | 95 | ]); |
| 91 | 96 | |
| 92 | - // Available dates endpoint (for frontend) | |
| 97 | + // Available dates endpoint (for frontend booking widget). | |
| 93 | 98 | register_rest_route($namespace, '/trips/(?P<trip_id>[\d]+)/available-dates', [ |
| 94 | 99 | [ |
| 95 | 100 | 'methods' => \WP_REST_Server::READABLE, |
| 96 | 101 | 'callback' => [$this, 'get_available_dates'], |
| @@ -97,20 +102,22 @@ | ||
| 97 | 102 | 'permission_callback' => '__return_true', // Public endpoint |
| 98 | 103 | ], |
| 99 | 104 | ]); |
| 100 | 105 | |
| 101 | - // Recurring rules endpoints | |
| 106 | + // Recurring rules — these are availability templates on the | |
| 107 | + // TRIP, not on individual departures. Gated on the trip-edit | |
| 108 | + // cap (same as the other availability controllers). | |
| 102 | 109 | $rulesBase = 'trips/(?P<trip_id>[\d]+)/recurring-rules'; |
| 103 | 110 | register_rest_route($namespace, '/' . $rulesBase, [ |
| 104 | 111 | [ |
| 105 | 112 | 'methods' => \WP_REST_Server::READABLE, |
| 106 | 113 | 'callback' => [$this, 'get_recurring_rules'], |
| 107 | - 'permission_callback' => [$this, 'check_permission'], | |
| 114 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 108 | 115 | ], |
| 109 | 116 | [ |
| 110 | 117 | 'methods' => \WP_REST_Server::CREATABLE, |
| 111 | 118 | 'callback' => [$this, 'create_recurring_rule'], |
| 112 | - 'permission_callback' => [$this, 'check_permission'], | |
| 119 | + 'permission_callback' => [$this, 'check_trip_edit_permission'], | |
| 113 | 120 | ], |
| 114 | 121 | ]); |
| 115 | 122 | |
| 116 | 123 | register_rest_route($namespace, '/' . $rulesBase . '/(?P<id>[\d]+)', [ |
| @@ -116,38 +123,75 @@ | ||
| 116 | 123 | register_rest_route($namespace, '/' . $rulesBase . '/(?P<id>[\d]+)', [ |
| 117 | 124 | [ |
| 118 | 125 | 'methods' => \WP_REST_Server::READABLE, |
| 119 | 126 | 'callback' => [$this, 'get_recurring_rule'], |
| 120 | - 'permission_callback' => [$this, 'check_permission'], | |
| 127 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 121 | 128 | ], |
| 122 | 129 | [ |
| 123 | 130 | 'methods' => \WP_REST_Server::EDITABLE, |
| 124 | 131 | 'callback' => [$this, 'update_recurring_rule'], |
| 125 | - 'permission_callback' => [$this, 'check_permission'], | |
| 132 | + 'permission_callback' => [$this, 'check_trip_edit_permission'], | |
| 126 | 133 | ], |
| 127 | 134 | [ |
| 128 | 135 | 'methods' => \WP_REST_Server::DELETABLE, |
| 129 | 136 | 'callback' => [$this, 'delete_recurring_rule'], |
| 130 | - 'permission_callback' => [$this, 'check_permission'], | |
| 137 | + 'permission_callback' => [$this, 'check_trip_edit_permission'], | |
| 131 | 138 | ], |
| 132 | 139 | ]); |
| 133 | 140 | |
| 134 | - // Preview recurring rule dates | |
| 141 | + // Preview recurring-rule dates — view cap. | |
| 135 | 142 | register_rest_route($namespace, '/' . $rulesBase . '/(?P<id>[\d]+)/preview', [ |
| 136 | 143 | [ |
| 137 | 144 | 'methods' => \WP_REST_Server::READABLE, |
| 138 | 145 | 'callback' => [$this, 'preview_recurring_rule'], |
| 139 | - 'permission_callback' => [$this, 'check_permission'], | |
| 146 | + 'permission_callback' => [$this, 'check_view_permission'], | |
| 140 | 147 | ], |
| 141 | 148 | ]); |
| 142 | 149 | } |
| 143 | 150 | |
| 144 | 151 | /** |
| 145 | - * Check permission | |
| 152 | + * Granular cap checks for every Departure endpoint. The previous | |
| 153 | + * implementation gated everything on `manage_options` which | |
| 154 | + * locked Sales Agent / Front Desk / Guide / Accountant / Auditor | |
| 155 | + * out of the departures REST surface despite the role bundles | |
| 156 | + * granting them view / manage / cancel caps. WP admins pass via | |
| 157 | + * the Team module's admin-fallback filter. | |
| 146 | 158 | */ |
| 159 | + public function check_view_permission(?WP_REST_Request $request = null): bool | |
| 160 | + { | |
| 161 | + return current_user_can('yatra_view_departures'); | |
| 162 | + } | |
| 163 | + | |
| 164 | + public function check_manage_permission(?WP_REST_Request $request = null): bool | |
| 165 | + { | |
| 166 | + // Held by Owner / Manager / Guide. Used for create + update. | |
| 167 | + return current_user_can('yatra_manage_departures'); | |
| 168 | + } | |
| 169 | + | |
| 170 | + public function check_cancel_permission(?WP_REST_Request $request = null): bool | |
| 171 | + { | |
| 172 | + // Held by Owner / Manager only by default. Cancelling a | |
| 173 | + // departure is a customer-affecting action (refunds, emails) | |
| 174 | + // so it gets the stricter cap than ordinary management. | |
| 175 | + return current_user_can('yatra_cancel_departures'); | |
| 176 | + } | |
| 177 | + | |
| 178 | + public function check_trip_edit_permission(?WP_REST_Request $request = null): bool | |
| 179 | + { | |
| 180 | + // Recurring rules belong to the parent trip, not to any one | |
| 181 | + // departure. Their lifecycle matches the trip-edit cap. | |
| 182 | + return current_user_can('yatra_edit_trips'); | |
| 183 | + } | |
| 184 | + | |
| 185 | + /** | |
| 186 | + * @deprecated Kept for any external code referencing the old | |
| 187 | + * method. Routes to view — safer default than the old | |
| 188 | + * `manage_options` shorthand. Admin users still pass via the | |
| 189 | + * admin-fallback layer. | |
| 190 | + */ | |
| 147 | 191 | public function check_permission(?WP_REST_Request $request = null): bool |
| 148 | 192 | { |
| 149 | - return current_user_can('manage_options'); | |
| 193 | + return $this->check_view_permission($request); | |
| 150 | 194 | } |
| 151 | 195 | |
| 152 | 196 | // ========================================================================= |
| 153 | 197 | // DEPARTURES ENDPOINTS |
| @@ -153,8 +197,26 @@ | ||
| 153 | 197 | // DEPARTURES ENDPOINTS |
| 154 | 198 | // ========================================================================= |
| 155 | 199 | |
| 156 | 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 | + /** | |
| 157 | 219 | * GET /trips/{trip_id}/departures |
| 158 | 220 | */ |
| 159 | 221 | public function get_departures(WP_REST_Request $request): WP_REST_Response |
| 160 | 222 | { |
| @@ -159,15 +221,21 @@ | ||
| 159 | 221 | public function get_departures(WP_REST_Request $request): WP_REST_Response |
| 160 | 222 | { |
| 161 | 223 | $tripId = (int) $request->get_param('trip_id'); |
| 162 | 224 | $status = $request->get_param('status'); |
| 225 | + $availability = $request->get_param('availability'); | |
| 226 | + $search = $request->get_param('search'); | |
| 163 | 227 | $source = $request->get_param('source'); |
| 164 | 228 | $dateFrom = $request->get_param('date_from'); |
| 165 | 229 | $dateTo = $request->get_param('date_to'); |
| 166 | 230 | $includePast = $request->get_param('include_past') !== 'false'; |
| 167 | - | |
| 231 | + | |
| 168 | 232 | $filters = []; |
| 169 | 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); | |
| 170 | 238 | if ($source) $filters['source'] = $source; |
| 171 | 239 | if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom; |
| 172 | 240 | if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo; |
| 173 | 241 | $filters['include_past'] = $includePast; |
| @@ -172,8 +240,17 @@ | ||
| 172 | 240 | if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo; |
| 173 | 241 | $filters['include_past'] = $includePast; |
| 174 | 242 | |
| 175 | 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); | |
| 176 | 253 | $departures = $this->departureService->getByTripId($tripId, $filters); |
| 177 | 254 | |
| 178 | 255 | // Get trip information |
| 179 | 256 | $tripRepository = new \Yatra\Repositories\TripRepository(); |
| @@ -192,14 +269,29 @@ | ||
| 192 | 269 | 'success' => true, |
| 193 | 270 | 'data' => array_map(function ($d) use ($trip, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) { |
| 194 | 271 | // Sync capacity from availability before returning |
| 195 | 272 | $date = $d->start_date ?: $d->date; |
| 196 | - $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date); | |
| 273 | + $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date, $d->time ?? null); | |
| 197 | 274 | if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) { |
| 198 | 275 | $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]); |
| 199 | 276 | $d->max_capacity = $correctCapacity; |
| 200 | 277 | } |
| 201 | - | |
| 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 | + | |
| 202 | 294 | $departureArray = $d->toArray(); |
| 203 | 295 | |
| 204 | 296 | // Add trip information |
| 205 | 297 | if ($trip) { |
| @@ -286,9 +378,12 @@ | ||
| 286 | 378 | // Debug: Log time and revenue values |
| 287 | 379 | return $departureArray; |
| 288 | 380 | }, $departures), |
| 289 | 381 | 'meta' => [ |
| 290 | - '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, | |
| 291 | 386 | ], |
| 292 | 387 | ]); |
| 293 | 388 | } catch (\Exception $e) { |
| 294 | 389 | return new WP_REST_Response([ |
| @@ -318,9 +413,9 @@ | ||
| 318 | 413 | |
| 319 | 414 | // Sync capacity from availability before returning |
| 320 | 415 | $capacityService = new \Yatra\Services\CapacityService(); |
| 321 | 416 | $date = $departure->start_date ?: $departure->date; |
| 322 | - $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date); | |
| 417 | + $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date, $departure->time ?? null); | |
| 323 | 418 | if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) { |
| 324 | 419 | $repo->update($departure->id, ['max_capacity' => $correctCapacity]); |
| 325 | 420 | $departure->max_capacity = $correctCapacity; |
| 326 | 421 | } |
| @@ -546,15 +641,21 @@ | ||
| 546 | 641 | */ |
| 547 | 642 | public function get_all_departures(WP_REST_Request $request): WP_REST_Response |
| 548 | 643 | { |
| 549 | 644 | $status = $request->get_param('status'); |
| 645 | + $availability = $request->get_param('availability'); | |
| 646 | + $search = $request->get_param('search'); | |
| 550 | 647 | $source = $request->get_param('source'); |
| 551 | 648 | $dateFrom = $request->get_param('date_from'); |
| 552 | 649 | $dateTo = $request->get_param('date_to'); |
| 553 | 650 | $includePast = $request->get_param('include_past') !== 'false'; |
| 554 | - | |
| 651 | + | |
| 555 | 652 | $filters = []; |
| 556 | 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); | |
| 557 | 658 | if ($source) $filters['source'] = $source; |
| 558 | 659 | if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom; |
| 559 | 660 | if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo; |
| 560 | 661 | $filters['include_past'] = $includePast; |
| @@ -559,8 +660,17 @@ | ||
| 559 | 660 | if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo; |
| 560 | 661 | $filters['include_past'] = $includePast; |
| 561 | 662 | |
| 562 | 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); | |
| 563 | 673 | // Get all departures (no trip filter) |
| 564 | 674 | $departures = $this->departureService->getAllDepartures($filters); |
| 565 | 675 | |
| 566 | 676 | // Get repository for additional data |
| @@ -576,14 +686,48 @@ | ||
| 576 | 686 | // Process each departure to add related data |
| 577 | 687 | $processed = array_map(function ($d) use ($tripRepository, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) { |
| 578 | 688 | // Sync capacity from availability before returning |
| 579 | 689 | $date = $d->start_date ?: $d->date; |
| 580 | - $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date); | |
| 581 | - if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) { | |
| 582 | - $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]); | |
| 583 | - $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 | + } | |
| 584 | 713 | } |
| 585 | - | |
| 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 | + | |
| 586 | 730 | $departureArray = $d->toArray(); |
| 587 | 731 | |
| 588 | 732 | // Add trip information |
| 589 | 733 | $trip = $tripRepository->find($d->trip_id); |
| @@ -670,9 +814,12 @@ | ||
| 670 | 814 | return new WP_REST_Response([ |
| 671 | 815 | 'success' => true, |
| 672 | 816 | 'data' => $processed, |
| 673 | 817 | 'meta' => [ |
| 674 | - '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, | |
| 675 | 822 | ], |
| 676 | 823 | ]); |
| 677 | 824 | } catch (\Exception $e) { |
| 678 | 825 | return new WP_REST_Response([ |
| @@ -713,13 +860,53 @@ | ||
| 713 | 860 | public function get_available_dates(WP_REST_Request $request): WP_REST_Response |
| 714 | 861 | { |
| 715 | 862 | $tripId = (int) $request->get_param('trip_id'); |
| 716 | 863 | $fromDate = $request->get_param('from_date') ?: date('Y-m-d'); |
| 717 | - $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(); | |
| 718 | 869 | |
| 719 | 870 | try { |
| 720 | 871 | $dates = $this->departureService->getAvailableDates($tripId, $fromDate, $toDate); |
| 721 | - | |
| 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 | + | |
| 722 | 909 | return new WP_REST_Response([ |
| 723 | 910 | 'success' => true, |
| 724 | 911 | 'data' => $dates, |
| 725 | 912 | ]); |