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/AvailabilityController.php +109 -25 3.0.5trunk View file →
@@ -82,8 +82,32 @@
82 82 'permission_callback' => [$this, 'check_permission'],
83 83 ],
84 84 ]);
85 85
86 + // Read-only list of dates a trip's recurring rules generate, for the
87 + // admin calendar. The main /availability list reads only the stored
88 + // availability_dates table, so a trip configured purely with recurring
89 + // rules showed an empty calendar. These are virtual (governed by the
90 + // rule, not individually editable), so the calendar renders them
91 + // read-only — hence a separate endpoint rather than mixing them into
92 + // the paginated, action-bearing /availability list.
93 + register_rest_route($namespace, '/' . $base . '/generated', [
94 + [
95 + 'methods' => \WP_REST_Server::READABLE,
96 + 'callback' => [$this, 'get_generated_dates'],
97 + 'permission_callback' => [$this, 'check_view_permission'],
98 + 'args' => [
99 + 'trip_id' => [
100 + 'required' => true,
101 + 'type' => 'integer',
102 + 'validate_callback' => function ($param) {
103 + return is_numeric($param) && $param > 0;
104 + },
105 + ],
106 + ],
107 + ],
108 + ]);
109 +
86 110 // Single item routes — view cap for read, edit cap for the
87 111 // mutations. Delete uses edit as well — there's no separate
88 112 // "delete availability date" cap in the registry because
89 113 // removing a date is functionally part of trip availability
@@ -166,32 +190,22 @@
166 190 if (!empty($availabilityIdByDate)) {
167 191 $this->service->updateBookingAvailabilityIds((int) $tripId, $availabilityIdByDate);
168 192 }
169 193
170 - // Aggregate bookings count per availability date for this trip
171 - // Use AvailabilityService to get booking counts
172 - $countsByAvailabilityId = [];
173 - $bookingCounts = $this->service->getBookingCountsByAvailabilityIds(array_column($items, 'id'));
174 -
175 - foreach ($bookingCounts as $row) {
176 - $aid = (int) ($row->availability_id ?? 0);
177 - if ($aid > 0) {
178 - $countsByAvailabilityId[$aid] = (int) ($row->booked_count ?? 0);
179 - }
180 - }
181 -
182 - $data = array_map(function ($item) use ($request, $countsByAvailabilityId) {
194 + $data = array_map(function ($item) use ($request) {
183 195 $prepared = $this->prepare_item_for_response($item, $request);
184 196
185 - $availabilityId = (int) ($prepared['id'] ?? 0);
186 - $bookedCount = 0;
197 + // Booked is derived from the bookings' own (trip, date, time)
198 + // identity, not the fragile availability_id join — see
199 + // AvailabilityService::getBookedCountForSlot. Each row passes its
200 + // own departure_time so multi-departure dates report per slot.
201 + $bookedCount = $this->service->getBookedCountForSlot(
202 + (int) ($prepared['trip_id'] ?? 0),
203 + (string) ($prepared['departure_date'] ?? ''),
204 + !empty($prepared['departure_time']) ? (string) $prepared['departure_time'] : null
205 + );
187 206
188 - if ($availabilityId > 0 && isset($countsByAvailabilityId[$availabilityId])) {
189 - $bookedCount = (int) $countsByAvailabilityId[$availabilityId];
190 - }
191 -
192 207 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
193 - $seatsReserved = (int) ($prepared['seats_reserved'] ?? 0);
194 208 $available = max(0, $seatsTotal - $bookedCount);
195 209
196 210 $prepared['booked_seats'] = $bookedCount;
197 211 $prepared['total_seats'] = $seatsTotal;
@@ -255,8 +269,76 @@
255 269 }
256 270 }
257 271
258 272 /**
273 + * Read-only dates generated by a trip's recurring rules, for the admin
274 + * calendar. Resolves through AvailabilityResolutionService so Booked /
275 + * Available reflect real bookings (same (trip, date, time) count the storefront
276 + * uses), and returns ONLY rule-generated dates — specific availability rows
277 + * already come from the main list, and trip-default (flexible) dates are left
278 + * out so this overlay is scoped to the recurring-rules gap it exists to fill.
279 + */
280 + public function get_generated_dates(WP_REST_Request $request)
281 + {
282 + try {
283 + $tripId = (int) $request->get_param('trip_id');
284 +
285 + // Fall back to sane defaults for missing OR malformed dates rather than
286 + // passing junk into the resolver (a bad date string errored the query).
287 + $normalizeDate = static function ($value, string $fallback): string {
288 + $value = sanitize_text_field((string) $value);
289 + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
290 + $ts = strtotime($value);
291 + if ($ts !== false && date('Y-m-d', $ts) === $value) {
292 + return $value;
293 + }
294 + }
295 + return $fallback;
296 + };
297 + $fromDate = $normalizeDate($request->get_param('from_date'), date('Y-m-d'));
298 + $toDate = $normalizeDate($request->get_param('to_date'), date('Y-m-d', strtotime('+12 months')));
299 +
300 + $resolver = new \Yatra\Services\AvailabilityResolutionService();
301 + $resolved = $resolver->getAllAvailabilityDates($tripId, $fromDate, $toDate);
302 +
303 + $dates = [];
304 + foreach ($resolved as $slot) {
305 + if (($slot->source ?? '') !== 'recurring_rule') {
306 + continue;
307 + }
308 + $total = (int) ($slot->seats_total ?? 0);
309 + $available = (int) ($slot->seats_available ?? 0);
310 + $dates[] = [
311 + 'id' => (string) ($slot->id ?? ''),
312 + 'trip_id' => $tripId,
313 + 'departure_date' => (string) ($slot->departure_date ?? ''),
314 + 'departure_time' => $slot->departure_time ?? null,
315 + 'arrival_date' => $slot->arrival_date ?? ($slot->departure_date ?? ''),
316 + 'arrival_time' => $slot->arrival_time ?? null,
317 + 'total_seats' => $total,
318 + 'seats_total' => $total,
319 + 'available_seats' => $available,
320 + 'seats_available' => $available,
321 + 'booked_seats' => max(0, $total - $available),
322 + 'waitlist_count' => 0,
323 + 'status' => (string) ($slot->status ?? 'available'),
324 + 'is_blocked' => !empty($slot->is_blocked),
325 + 'original_price' => $slot->original_price ?? null,
326 + 'discounted_price' => $slot->discounted_price ?? null,
327 + // Marks this as a read-only, rule-generated entry so the
328 + // calendar shows it without edit/delete affordances.
329 + 'is_virtual' => true,
330 + 'source' => 'rule',
331 + ];
332 + }
333 +
334 + return new WP_REST_Response(['dates' => $dates, 'total' => count($dates)], 200);
335 + } catch (\Exception $e) {
336 + return new WP_Error('availability_generated_error', $e->getMessage(), ['status' => 500]);
337 + }
338 + }
339 +
340 + /**
259 341 * Get single availability date
260 342 */
261 343 public function get_item(WP_REST_Request $request)
262 344 {
@@ -273,16 +355,18 @@
273 355 }
274 356
275 357 $prepared = $this->prepare_item_for_response($item, $request);
276 358
277 - // Compute live booked seats for this availability_id
359 + // Booked derived from the bookings' (trip, date, time) identity, the
360 + // same way the list does — not the fragile availability_id join.
278 361 if (!empty($prepared['id'])) {
362 + $bookedCount = $this->service->getBookedCountForSlot(
363 + (int) ($prepared['trip_id'] ?? 0),
364 + (string) ($prepared['departure_date'] ?? ''),
365 + !empty($prepared['departure_time']) ? (string) $prepared['departure_time'] : null
366 + );
279 367
280 - // Use AvailabilityService to get booked count
281 - $bookedCount = $this->service->getBookedCountByAvailabilityId((int) $prepared['id']);
282 -
283 368 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
284 -
285 369 $available = max(0, $seatsTotal - $bookedCount);
286 370
287 371 $prepared['booked_seats'] = $bookedCount;
288 372 $prepared['seats_available'] = $available;