| @@ -396,13 +396,14 @@ | ||
| 396 | 396 | { |
| 397 | 397 | // Use centralized AvailabilityResolutionService |
| 398 | 398 | $resolutionService = new \Yatra\Services\AvailabilityResolutionService(); |
| 399 | 399 | |
| 400 | - // Get dates for next 12 months | |
| 400 | + // From today up to the configurable booking horizon (Settings → Booking; | |
| 401 | + // 12 months unless changed — the previous hard-coded value). | |
| 401 | 402 | $fromDate = date('Y-m-d'); |
| 402 | - $toDate = date('Y-m-d', strtotime('+12 months')); | |
| 403 | + $toDate = yatra_get_availability_horizon_date($fromDate); | |
| 403 | 404 | |
| 404 | - $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate); | |
| 405 | + $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate, \Yatra\Services\SettingsService::isEnabled('show_sold_out')); | |
| 405 | 406 | |
| 406 | 407 | // Add calculated fields |
| 407 | 408 | foreach ($availability as $avail) { |
| 408 | 409 | // Calculate if limited availability |
| @@ -466,8 +467,9 @@ | ||
| 466 | 467 | 'age_min' => isset($meta['age_min']) ? (int) $meta['age_min'] : null, |
| 467 | 468 | 'age_max' => isset($meta['age_max']) ? (int) $meta['age_max'] : null, |
| 468 | 469 | 'min_pax' => isset($meta['min_pax']) ? (int) $meta['min_pax'] : null, |
| 469 | 470 | 'max_pax' => isset($meta['max_pax']) ? (int) $meta['max_pax'] : null, |
| 471 | + 'group_overflow' => isset($meta['group_overflow']) && in_array($meta['group_overflow'], ['block', 'per_block'], true) ? $meta['group_overflow'] : 'block', | |
| 470 | 472 | 'max_quantity' => isset($meta['max_quantity']) ? (int) $meta['max_quantity'] : null, |
| 471 | 473 | 'description' => $meta['description'] ?? '', |
| 472 | 474 | ]; |
| 473 | 475 | } |
| @@ -497,8 +499,9 @@ | ||
| 497 | 499 | 'age_min' => $cat ? $cat->age_min : null, |
| 498 | 500 | 'age_max' => $cat ? $cat->age_max : null, |
| 499 | 501 | 'min_pax' => $cat ? $cat->min_pax : null, |
| 500 | 502 | 'max_pax' => $cat ? $cat->max_pax : null, |
| 503 | + 'group_overflow' => $cat ? $cat->group_overflow : ($pt['group_overflow'] ?? 'block'), | |
| 501 | 504 | 'max_quantity' => $cat ? $cat->max_quantity : null, |
| 502 | 505 | 'description' => $cat ? $cat->description : ($pt['description'] ?? ''), |
| 503 | 506 | ]; |
| 504 | 507 | } |
| @@ -1092,9 +1095,13 @@ | ||
| 1092 | 1095 | $patterns = [ |
| 1093 | 1096 | '/youtube\.com\/watch\?v=([^&]+)/', |
| 1094 | 1097 | '/youtube\.com\/embed\/([^?]+)/', |
| 1095 | 1098 | '/youtu\.be\/([^?]+)/', |
| 1096 | - '/youtube\.com\/v\/([^?]+)/' | |
| 1099 | + '/youtube\.com\/v\/([^?]+)/', | |
| 1100 | + // Shorts + live URLs (e.g. youtube.com/shorts/XFR9Ti-4RbM?si=...). | |
| 1101 | + // ID stops at ?, & or / so trailing query params are excluded. | |
| 1102 | + '/youtube\.com\/shorts\/([^?&\/]+)/', | |
| 1103 | + '/youtube\.com\/live\/([^?&\/]+)/' | |
| 1097 | 1104 | ]; |
| 1098 | 1105 | |
| 1099 | 1106 | foreach ($patterns as $pattern) { |
| 1100 | 1107 | if (preg_match($pattern, $url, $matches)) { |
| @@ -1226,13 +1233,19 @@ | ||
| 1226 | 1233 | */ |
| 1227 | 1234 | private function getSimilarTrips(object $trip): array |
| 1228 | 1235 | { |
| 1229 | 1236 | $trip_id = (int) $trip->id; |
| 1237 | + | |
| 1238 | + // Hour-based day tours (3.0.14+ column) — guarded so an install whose | |
| 1239 | + // upgrade ALTER has not run yet keeps rendering similar trips. | |
| 1240 | + $duration_hours_col = (new \Yatra\Repositories\TripRepository())->hasTripColumn('duration_hours') | |
| 1241 | + ? ', duration_hours' | |
| 1242 | + : ''; | |
| 1230 | 1243 | |
| 1231 | 1244 | // Get similar trips based on category or difficulty |
| 1232 | 1245 | $similar = $this->wpdb->get_results( |
| 1233 | 1246 | $this->wpdb->prepare( |
| 1234 | - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights, | |
| 1247 | + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col}, | |
| 1235 | 1248 | original_price, sale_price, difficulty_level, |
| 1236 | 1249 | short_description |
| 1237 | 1250 | FROM {$this->table_trips} t |
| 1238 | 1251 | WHERE t.id != %d |
| @@ -1263,9 +1276,9 @@ | ||
| 1263 | 1276 | // Fallback: Get any published trips if no similar found |
| 1264 | 1277 | if (empty($similar)) { |
| 1265 | 1278 | $similar = $this->wpdb->get_results( |
| 1266 | 1279 | $this->wpdb->prepare( |
| 1267 | - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights, | |
| 1280 | + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col}, | |
| 1268 | 1281 | original_price, sale_price, difficulty_level, |
| 1269 | 1282 | short_description |
| 1270 | 1283 | FROM {$this->table_trips} |
| 1271 | 1284 | WHERE id != %d |
| @@ -1884,8 +1897,16 @@ | ||
| 1884 | 1897 | |
| 1885 | 1898 | $input_id = 'traveler_' . $price_type->category_id; |
| 1886 | 1899 | $max_travelers = is_object($trip) && method_exists($trip, 'getMaxTravelers') ? $trip->getMaxTravelers() : ($trip->max_travelers ?? 20); |
| 1887 | 1900 | $pt_max_qty = (int) ($price_type->max_quantity ?: $max_travelers); |
| 1901 | + // A per-group category in "block" overflow mode caps the party at | |
| 1902 | + // the max group size. In "per_block" mode the party may exceed it | |
| 1903 | + // (it just buys additional group blocks), so we keep the trip's | |
| 1904 | + // normal cap there. | |
| 1905 | + if ($is_per_group && !empty($price_type->max_pax) | |
| 1906 | + && (($price_type->group_overflow ?? 'block') !== 'per_block')) { | |
| 1907 | + $pt_max_qty = (int) $price_type->max_pax; | |
| 1908 | + } | |
| 1888 | 1909 | $pt_value = ($index === $default_index) ? 1 : 0; |
| 1889 | 1910 | |
| 1890 | 1911 | $traveler_rows[] = [ |
| 1891 | 1912 | 'label' => $price_type->category_label ?: __('Traveler', 'yatra'), |
| @@ -1894,8 +1915,10 @@ | ||
| 1894 | 1915 | 'row_attrs' => [ |
| 1895 | 1916 | 'data-category-id' => $price_type->category_id, |
| 1896 | 1917 | 'data-price' => $price_type->effective_price, |
| 1897 | 1918 | 'data-pricing-mode' => $pricing_mode, |
| 1919 | + 'data-group-overflow' => $price_type->group_overflow ?? 'block', | |
| 1920 | + 'data-max-pax' => $price_type->max_pax ?? '', | |
| 1898 | 1921 | ], |
| 1899 | 1922 | 'minus_disabled' => ($index !== $default_index), |
| 1900 | 1923 | 'plus_disabled' => false, |
| 1901 | 1924 | 'minus_attrs' => [ |
| @@ -1917,8 +1940,10 @@ | ||
| 1917 | 1940 | 'data-category' => $price_type->category_id, |
| 1918 | 1941 | 'data-category-label' => $price_type->category_label, |
| 1919 | 1942 | 'data-price' => $price_type->effective_price, |
| 1920 | 1943 | 'data-pricing-mode' => $pricing_mode, |
| 1944 | + 'data-group-overflow' => $price_type->group_overflow ?? 'block', | |
| 1945 | + 'data-max-pax' => $price_type->max_pax ?? '', | |
| 1921 | 1946 | ], |
| 1922 | 1947 | ]; |
| 1923 | 1948 | } |
| 1924 | 1949 | |
| @@ -2093,9 +2118,15 @@ | ||
| 2093 | 2118 | $pt_min_qty = 0; |
| 2094 | 2119 | $pt_max_qty = (int) min($seats_available, $max_travelers); |
| 2095 | 2120 | $pt_pricing_mode = $pt->pricing_mode ?? 'per_person'; |
| 2096 | 2121 | $pt_is_per_group = ($pt_pricing_mode === 'per_group'); |
| 2097 | - | |
| 2122 | + // Cap a per-group "block" category at its max group size (still | |
| 2123 | + // bounded by seats). "per_block" mode may exceed it, so skip. | |
| 2124 | + if ($pt_is_per_group && !empty($pt->max_pax) | |
| 2125 | + && (($pt->group_overflow ?? 'block') !== 'per_block')) { | |
| 2126 | + $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax); | |
| 2127 | + } | |
| 2128 | + | |
| 2098 | 2129 | // Build pricing label |
| 2099 | 2130 | $pricing_label = ''; |
| 2100 | 2131 | if ($pt_is_per_group) { |
| 2101 | 2132 | if (!empty($pt->min_pax) && !empty($pt->max_pax)) { |
| @@ -2126,8 +2157,10 @@ | ||
| 2126 | 2157 | 'row_attrs' => [ |
| 2127 | 2158 | 'data-category-id' => $pt_category_id, |
| 2128 | 2159 | 'data-price' => $pt_price, |
| 2129 | 2160 | 'data-pricing-mode' => $pt_pricing_mode, |
| 2161 | + 'data-group-overflow' => $pt->group_overflow ?? 'block', | |
| 2162 | + 'data-max-pax' => $pt->max_pax ?? '', | |
| 2130 | 2163 | ], |
| 2131 | 2164 | 'minus_disabled' => ($pt_default <= 0), |
| 2132 | 2165 | 'plus_disabled' => false, |
| 2133 | 2166 | 'minus_attrs' => [ |
| @@ -2144,8 +2177,10 @@ | ||
| 2144 | 2177 | 'data-item' => $item_id, |
| 2145 | 2178 | 'data-category' => $pt_category_id, |
| 2146 | 2179 | 'data-price' => $pt_price, |
| 2147 | 2180 | 'data-pricing-mode' => $pt_pricing_mode, |
| 2181 | + 'data-group-overflow' => $pt->group_overflow ?? 'block', | |
| 2182 | + 'data-max-pax' => $pt->max_pax ?? '', | |
| 2148 | 2183 | 'value' => $pt_default, |
| 2149 | 2184 | 'min' => $pt_min_qty, |
| 2150 | 2185 | 'max' => $pt_max_qty, |
| 2151 | 2186 | ], |