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 | templates/partials/availability-card.php +111 -17 3.0.4 → 3.0.16 View file →
@@ -27,8 +27,29 @@
27 27 if ($is_sold_out || $card_status === 'sold_out') {
28 28 $card_status = 'sold_out';
29 29 }
30 30
31 + // A sold-out departure is only bookable as a WAITLIST entry, and only
32 + // when the server would actually accept it — the same gate
33 + // BookingSessionController applies (WaitlistService::canJoinWaitlist).
34 + // When waitlist is unavailable the booking is rejected at the final
35 + // step, so the Book Now button must be blocked here rather than
36 + // failing late (the reported bug). Probing with 1 traveler is
37 + // sufficient: canJoinWaitlist requires seats_available < N, which for a
38 + // sold-out card (0 seats) holds for any N >= 1.
39 + $waitlist_available = false;
40 + if ($is_sold_out && class_exists('\\Yatra\\Services\\WaitlistService')) {
41 + $waitlist_probe = (object) [
42 + 'id' => (int) ($card['id'] ?? 0),
43 + 'seats_available' => $seats_available,
44 + ];
45 + $waitlist_available = \Yatra\Services\WaitlistService::canJoinWaitlist(
46 + $trip_data,
47 + $waitlist_probe,
48 + 1
49 + );
50 + }
51 +
31 52 // Match Y-m-d even if DB returns datetime (e.g. 2026-08-13 00:00:00)
32 53 $yatra_card_date_norm = static function ($v): string {
33 54 $v = trim((string) $v);
34 55 return preg_match('/^(\d{4}-\d{2}-\d{2})/', $v, $m) ? $m[1] : $v;
@@ -185,15 +206,15 @@
185 206 <div class="yatra-card-header-sub"><?php esc_html_e('per departure', 'yatra'); ?></div>
186 207 </div>
187 208
188 209 <!-- Price (sale only; struck original + % badge sit in .yatra-card-badges-container with status) -->
189 - <div class="yatra-card-header-price">
210 + <div class="yatra-card-header-price" data-price-unit="<?php echo esc_attr($card['price_unit'] ?? 'per_person'); ?>">
190 211 <div class="yatra-card-price-group yatra-card-price-group--stack">
191 212 <?php
192 213 echo '<span class="yatra-sale-price">' . yatra_format_price($display_sale_price) . '</span>';
193 214 ?>
194 215 </div>
195 - <span class="yatra-price-label"><?php echo esc_html__('per person', 'yatra'); ?></span>
216 + <span class="yatra-price-label"><?php echo esc_html($card['price_unit_label'] ?? __('per person', 'yatra')); ?></span>
196 217 </div>
197 218
198 219 <!-- Toggle Arrow -->
199 220 <div class="yatra-card-header-arrow">
@@ -215,10 +236,22 @@
215 236 <div class="yatra-card-info-content">
216 237 <div class="yatra-card-info-label"><?php esc_html_e('Duration', 'yatra'); ?></div>
217 238 <div class="yatra-card-info-value">
218 239 <?php
219 - $duration_days = (int) ($trip_data->duration_days ?? 1);
220 - echo esc_html($duration_days . ' ' . _n('Day', 'Days', $duration_days, 'yatra'));
240 + // Prefer the per-card span (matches this card's departure/return
241 + // dates); fall back to the trip default for sample cards.
242 + $duration_days = (int) ($card['duration_days'] ?? $trip_data->duration_days ?? 1);
243 + $duration_hours = (int) ($trip_data->duration_hours ?? 0);
244 + if ($duration_hours > 0) {
245 + // Hour-based day tour: "8 Hours", not "1 Day".
246 + echo esc_html(sprintf(
247 + /* translators: %d: number of hours. */
248 + _n('%d Hour', '%d Hours', $duration_hours, 'yatra'),
249 + $duration_hours
250 + ));
251 + } else {
252 + echo esc_html($duration_days . ' ' . _n('Day', 'Days', $duration_days, 'yatra'));
253 + }
221 254 ?>
222 255 </div>
223 256 </div>
224 257 </div>
@@ -228,9 +261,12 @@
228 261 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
229 262 </div>
230 263 <div class="yatra-card-info-content">
231 264 <div class="yatra-card-info-label"><?php esc_html_e('Seats left', 'yatra'); ?></div>
232 - <div class="yatra-card-info-value"><?php echo esc_html(sprintf(_n('%d seat', '%d seats', $seats_available, 'yatra'), $seats_available)); ?></div>
265 + <div class="yatra-card-info-value"><?php
266 + /* translators: %d: number of seats remaining. */
267 + echo esc_html(sprintf(_n('%d seat', '%d seats', $seats_available, 'yatra'), $seats_available));
268 + ?></div>
233 269 </div>
234 270 </div>
235 271
236 272 <?php
@@ -251,9 +287,10 @@
251 287 if (preg_match('/(\d+)\s*(hour|day|week)s?\s*before/i', $cancellation_policy, $matches)) {
252 288 $time_value = $matches[1];
253 289 $time_unit = $matches[2];
254 290 $cancellation_value = sprintf(
255 - __('Up to %d %s%s before', 'yatra'),
291 + /* translators: 1: numeric amount, 2: time unit (hour/day/week), 3: plural "s" suffix if applicable. */
292 + __('Up to %1$d %2$s%3$s before', 'yatra'),
256 293 $time_value,
257 294 $time_unit,
258 295 $time_value > 1 ? 's' : ''
259 296 );
@@ -319,8 +356,21 @@
319 356 $pt_category_id = $pt->category_id ?? $pt_index;
320 357 $pt_default = $pt_index === 0 ? 1 : 0;
321 358 $pt_min_qty = 0;
322 359 $pt_max_qty = (int) min($seats_available, $max_travelers);
360 + // Cap a per-group "block" category at its max group
361 + // size (still bounded by seats). "per_block" mode may
362 + // exceed it (additional group blocks), so skip.
363 + if ($pt_is_per_group && !empty($pt->max_pax)
364 + && (($pt->group_overflow ?? 'block') !== 'per_block')) {
365 + $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax);
366 + }
367 + // A sold-out departure has no seats left to offer, so the
368 + // first category must not still open at 1 traveler. Clamping
369 + // here also disables both +/- buttons below (0 <= 0 >= 0).
370 + if ($pt_default > $pt_max_qty) {
371 + $pt_default = $pt_max_qty;
372 + }
323 373
324 374 // Determine price with fallback chain
325 375 $pt_price = 0;
326 376 if (isset($pt->effective_price) && $pt->effective_price > 0) {
@@ -353,12 +403,15 @@
353 403 $pt_age_min = isset($pt->age_min) ? (int) $pt->age_min : null;
354 404 $pt_age_max = isset($pt->age_max) ? (int) $pt->age_max : null;
355 405 if ($pt_age_min !== null || $pt_age_max !== null) {
356 406 if ($pt_age_min !== null && $pt_age_max !== null) {
357 - $pt_age_text = sprintf(__('(Age %d-%d)', 'yatra'), $pt_age_min, $pt_age_max);
407 + /* translators: 1: minimum age, 2: maximum age. */
408 + $pt_age_text = sprintf(__('(Age %1$d-%2$d)', 'yatra'), $pt_age_min, $pt_age_max);
358 409 } elseif ($pt_age_min !== null) {
410 + /* translators: %d: minimum age. */
359 411 $pt_age_text = sprintf(__('(Age %d+)', 'yatra'), $pt_age_min);
360 412 } else {
413 + /* translators: %d: maximum age. */
361 414 $pt_age_text = sprintf(__('(Up to age %d)', 'yatra'), $pt_age_max);
362 415 }
363 416 }
364 417
@@ -365,12 +418,15 @@
365 418 // Build pricing label for per_group mode
366 419 $pricing_label = '';
367 420 if ($pt_is_per_group) {
368 421 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
369 - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
422 + /* translators: 1: minimum pax for the group price, 2: maximum pax. */
423 + $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
370 424 } elseif (!empty($pt->max_pax)) {
425 + /* translators: %d: maximum pax for the group price. */
371 426 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
372 427 } elseif (!empty($pt->min_pax)) {
428 + /* translators: %d: minimum pax for the group price. */
373 429 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
374 430 } else {
375 431 $pricing_label = __('per group', 'yatra');
376 432 }
@@ -396,8 +452,10 @@
396 452 'row_attrs' => [
397 453 'data-category-id' => $pt_category_id,
398 454 'data-price' => $pt_price,
399 455 'data-pricing-mode' => $pt_pricing_mode,
456 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
457 + 'data-max-pax' => $pt->max_pax ?? '',
400 458 ],
401 459 'minus_disabled' => $pt_default <= $pt_min_qty,
402 460 'plus_disabled' => $pt_default >= $pt_max_qty,
403 461 'minus_attrs' => [
@@ -403,8 +461,9 @@
403 461 'minus_attrs' => [
404 462 'data-target' => $input_id,
405 463 'data-item' => $item_id,
406 464 'data-category' => $pt_category_id,
465 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
407 466 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
408 467 ],
409 468 'plus_attrs' => [
410 469 'data-target' => $input_id,
@@ -409,8 +468,9 @@
409 468 'plus_attrs' => [
410 469 'data-target' => $input_id,
411 470 'data-item' => $item_id,
412 471 'data-category' => $pt_category_id,
472 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
413 473 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
414 474 ],
415 475 'input_attrs' => [
416 476 'id' => $input_id,
@@ -422,8 +482,10 @@
422 482 'data-category' => $pt_category_id,
423 483 'data-category-label' => $pt_label,
424 484 'data-price' => $pt_price,
425 485 'data-pricing-mode' => $pt_pricing_mode,
486 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
487 + 'data-max-pax' => $pt->max_pax ?? '',
426 488 ],
427 489 ];
428 490
429 491 if ($pt_default > 0) {
@@ -459,8 +521,17 @@
459 521 include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php';
460 522 ?>
461 523 <?php else: ?>
462 524 <!-- Regular pricing: Show simple number of travelers -->
525 + <?php
526 + // A sold-out departure offers no seats, so the counter must start
527 + // at 0 (not 1) and both controls stay disabled. Only the ceiling
528 + // was capacity-aware before, which left sold-out cards showing a
529 + // selectable traveler.
530 + $simple_max_qty = (int) min($seats_available, $max_travelers);
531 + $simple_qty = $simple_max_qty > 0 ? 1 : 0;
532 + $simple_min_qty = $simple_qty;
533 + ?>
463 534 <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>">
464 535 <div class="yatra-booking-field-icon">
465 536 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
466 537 </div>
@@ -469,18 +540,18 @@
469 540 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
470 541 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
471 542 </svg>
472 543 </button>
473 - <input type="number"
474 - id="num-travelers-<?php echo esc_attr($item_id); ?>"
475 - class="yatra-quantity-input-simple yatra-availability-num-travelers"
544 + <input type="number"
545 + id="num-travelers-<?php echo esc_attr($item_id); ?>"
546 + class="yatra-quantity-input-simple yatra-availability-num-travelers"
476 547 data-item="<?php echo esc_attr($item_id); ?>"
477 - value="1"
478 - min="1"
479 - max="<?php echo esc_attr(min($seats_available, $max_travelers)); ?>"
548 + value="<?php echo esc_attr($simple_qty); ?>"
549 + min="<?php echo esc_attr($simple_min_qty); ?>"
550 + max="<?php echo esc_attr($simple_max_qty); ?>"
480 551 readonly
481 552 data-price="<?php echo esc_attr($display_sale_price); ?>">
482 - <button type="button" class="yatra-quantity-btn yatra-quantity-plus" data-target="num-travelers-<?php echo esc_attr($item_id); ?>" aria-label="<?php esc_attr_e('Increase travelers', 'yatra'); ?>">
553 + <button type="button" class="yatra-quantity-btn yatra-quantity-plus" data-target="num-travelers-<?php echo esc_attr($item_id); ?>" aria-label="<?php esc_attr_e('Increase travelers', 'yatra'); ?>"<?php echo $simple_qty >= $simple_max_qty ? ' disabled' : ''; ?>>
483 554 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
484 555 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
485 556 </svg>
486 557 </button>
@@ -535,10 +606,27 @@
535 606
536 607 // Is this a day trip?
537 608 $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']);
538 609 ?>
539 - <button type="button"
540 - class="yatra-card-book-btn"
610 + <?php if ($is_sold_out && !$waitlist_available): ?>
611 + <?php // Sold out and no waitlist available: block booking here.
612 + // The server would reject this at the final step
613 + // (BookingSessionController → code 'sold_out'), so we
614 + // disable the button instead of letting the customer go
615 + // all the way through and fail. ?>
616 + <button type="button"
617 + class="yatra-card-book-btn yatra-card-book-btn--sold-out"
618 + disabled
619 + aria-disabled="true"
620 + data-item="<?php echo esc_attr($item_id); ?>">
621 + <span class="yatra-card-book-btn-text">
622 + <span class="yatra-card-book-btn-title"><?php esc_html_e('Sold Out', 'yatra'); ?></span>
623 + <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('No seats available', 'yatra'); ?></span>
624 + </span>
625 + </button>
626 + <?php else: ?>
627 + <button type="button"
628 + class="yatra-card-book-btn<?php echo ($is_sold_out && $waitlist_available) ? ' yatra-card-book-btn--waitlist' : ''; ?>"
541 629 data-trip-id="<?php echo esc_attr($trip_id); ?>"
542 630 data-availability-id="<?php echo esc_attr($item_id); ?>"
543 631 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
544 632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
@@ -548,11 +636,17 @@
548 636 data-price="<?php echo esc_attr($display_sale_price); ?>"
549 637 data-item="<?php echo esc_attr($item_id); ?>">
550 638 <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?>
551 639 <span class="yatra-card-book-btn-text">
640 + <?php if ($is_sold_out && $waitlist_available): ?>
641 + <span class="yatra-card-book-btn-title"><?php esc_html_e('Join Waitlist', 'yatra'); ?></span>
642 + <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Sold out — join the waitlist', 'yatra'); ?></span>
643 + <?php else: ?>
552 644 <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span>
553 645 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span>
646 + <?php endif; ?>
554 647 </span>
555 648 </button>
649 + <?php endif; ?>
556 650 </div>
557 651 </div>
558 652 </div>