| @@ -18,8 +18,9 @@ | ||
| 18 | 18 | use Yatra\Utils\Cache; |
| 19 | 19 | use Yatra\Utils\QueryCache; |
| 20 | 20 | use Yatra\Database\Tables\TripAvailabilityDatesTable; |
| 21 | 21 | use Yatra\Database\Tables\TripAvailabilityRulesTable; |
| 22 | +use Yatra\Database\Tables\DeparturesTable; | |
| 22 | 23 | use Yatra\Constants\ClassificationTypes; |
| 23 | 24 | |
| 24 | 25 | /** |
| 25 | 26 | * Trip Repository |
| @@ -270,8 +271,9 @@ | ||
| 270 | 271 | * Create trip row; {@see afterWrite} handles cache; fires action `yatra_trip_created` once per insert. |
| 271 | 272 | */ |
| 272 | 273 | public function create(array $data): int |
| 273 | 274 | { |
| 275 | + $data = $this->dropUnsupportedDurationHours($data); | |
| 274 | 276 | $id = parent::create($data); |
| 275 | 277 | do_action('yatra_trip_created', $id); |
| 276 | 278 | |
| 277 | 279 | return $id; |
| @@ -277,12 +279,32 @@ | ||
| 277 | 279 | return $id; |
| 278 | 280 | } |
| 279 | 281 | |
| 280 | 282 | /** |
| 283 | + * Defensive: if the `duration_hours` column is somehow missing (a failed or | |
| 284 | + * pending column-add migration on a restrictive host), drop it from the | |
| 285 | + * write set so a normal trip save still succeeds instead of erroring on an | |
| 286 | + * unknown column. The hour-based feature simply stays off until the column | |
| 287 | + * exists — it never breaks saving. | |
| 288 | + * | |
| 289 | + * @param array<string, mixed> $data | |
| 290 | + * @return array<string, mixed> | |
| 291 | + */ | |
| 292 | + protected function dropUnsupportedDurationHours(array $data): array | |
| 293 | + { | |
| 294 | + if (array_key_exists('duration_hours', $data) && !$this->tripTableHasColumn('duration_hours')) { | |
| 295 | + unset($data['duration_hours']); | |
| 296 | + } | |
| 297 | + | |
| 298 | + return $data; | |
| 299 | + } | |
| 300 | + | |
| 301 | + /** | |
| 281 | 302 | * Override update method to provide proper field formats |
| 282 | 303 | */ |
| 283 | 304 | public function update(int $id, array $data): bool |
| 284 | 305 | { |
| 306 | + $data = $this->dropUnsupportedDurationHours($data); | |
| 285 | 307 | $data = $this->sanitizeData($data); |
| 286 | 308 | $data['updated_at'] = current_time('mysql'); |
| 287 | 309 | |
| 288 | 310 | // Build format array based on field types |
| @@ -400,8 +422,21 @@ | ||
| 400 | 422 | return self::$tripColumnExistsCache[$column]; |
| 401 | 423 | } |
| 402 | 424 | |
| 403 | 425 | /** |
| 426 | + * Public, cached column-existence check for the trips table. | |
| 427 | + * | |
| 428 | + * Lets raw SELECTs outside this repository (booking confirmation join, | |
| 429 | + * similar-trips query) add columns introduced by later upgrades — e.g. | |
| 430 | + * `duration_hours` — without failing on an install whose ALTER has not | |
| 431 | + * run yet (see InstallerService::maybeAddTripDurationHoursColumn()). | |
| 432 | + */ | |
| 433 | + public function hasTripColumn(string $column): bool | |
| 434 | + { | |
| 435 | + return $this->tripTableHasColumn($column); | |
| 436 | + } | |
| 437 | + | |
| 438 | + /** | |
| 404 | 439 | * SQL expression for trip "current" list price (matches TripPricingService::resolveRegularCurrentPrice). |
| 405 | 440 | */ |
| 406 | 441 | protected function sqlTripEffectiveListPrice(): string |
| 407 | 442 | { |
| @@ -656,20 +691,21 @@ | ||
| 656 | 691 | foreach (array_unique($filters['age_suitability']) as $age) { |
| 657 | 692 | if (!is_string($age)) { |
| 658 | 693 | continue; |
| 659 | 694 | } |
| 695 | + $ageLimits = self::ageSuitabilityThresholds(); | |
| 660 | 696 | switch ($age) { |
| 661 | 697 | case 'family-friendly': |
| 662 | - $ageParts[] = '(t.age_min IS NULL OR t.age_min <= 5)'; | |
| 698 | + $ageParts[] = '(t.age_min IS NULL OR t.age_min <= ' . (int) $ageLimits['family_max'] . ')'; | |
| 663 | 699 | break; |
| 664 | 700 | case 'kids-friendly': |
| 665 | - $ageParts[] = '(t.age_min IS NULL OR t.age_min <= 12)'; | |
| 701 | + $ageParts[] = '(t.age_min IS NULL OR t.age_min <= ' . (int) $ageLimits['kids_max'] . ')'; | |
| 666 | 702 | break; |
| 667 | 703 | case 'senior-friendly': |
| 668 | - $ageParts[] = '(t.age_max IS NULL OR t.age_max >= 65)'; | |
| 704 | + $ageParts[] = '(t.age_max IS NULL OR t.age_max >= ' . (int) $ageLimits['senior_min'] . ')'; | |
| 669 | 705 | break; |
| 670 | 706 | case 'adults-only': |
| 671 | - $ageParts[] = 't.age_min >= 18'; | |
| 707 | + $ageParts[] = 't.age_min >= ' . (int) $ageLimits['adults_min']; | |
| 672 | 708 | break; |
| 673 | 709 | } |
| 674 | 710 | } |
| 675 | 711 | if ($ageParts !== []) { |
| @@ -810,9 +846,29 @@ | ||
| 810 | 846 | if (!empty($filters['duration_max']) && $filters['duration_max'] > 0) { |
| 811 | 847 | $wheres[] = "CAST(t.duration_days AS UNSIGNED) <= %d"; |
| 812 | 848 | $params[] = $filters['duration_max']; |
| 813 | 849 | } |
| 814 | - | |
| 850 | + | |
| 851 | + // Availability date filter ("available from this date onward"): keep only | |
| 852 | + // trips that have a departure starting ON OR AFTER the selected date, so | |
| 853 | + // a customer who picks a date sees every trip they could still travel on | |
| 854 | + // from then. A departure matches when its effective start date (the | |
| 855 | + // explicit start_date, else the single `date`) is >= the chosen day and | |
| 856 | + // it has not been cancelled — the date bound alone excludes past dates | |
| 857 | + // when the picker's min=today is respected, so no reliance on a possibly | |
| 858 | + // stale 'past' status. Clause + param are appended together so the WHERE | |
| 859 | + // ordering stays in sync with $params (see the count/main queries). | |
| 860 | + if (!empty($filters['available_date'])) { | |
| 861 | + $departures_table = DeparturesTable::getTableName(); | |
| 862 | + $wheres[] = "EXISTS ( | |
| 863 | + SELECT 1 FROM {$departures_table} yd | |
| 864 | + WHERE yd.trip_id = t.id | |
| 865 | + AND COALESCE(NULLIF(yd.start_date, '0000-00-00'), yd.date) >= %s | |
| 866 | + AND yd.status <> 'cancelled' | |
| 867 | + )"; | |
| 868 | + $params[] = $filters['available_date']; | |
| 869 | + } | |
| 870 | + | |
| 815 | 871 | // Rating filter |
| 816 | 872 | if (!empty($filters['rating_min']) && $filters['rating_min'] > 0) { |
| 817 | 873 | $having_clauses[] = "AVG(r.rating) >= %f"; |
| 818 | 874 | $rating_params[] = $filters['rating_min']; |
| @@ -1764,8 +1820,28 @@ | ||
| 1764 | 1820 | */ |
| 1765 | 1821 | public function savePriceTypes(int $tripId, array $priceTypes): void |
| 1766 | 1822 | { |
| 1767 | 1823 | if (empty($priceTypes)) { |
| 1824 | + // Explicitly clear stored per-category pricing so that deleting all | |
| 1825 | + // rows (even the last one) persists. Previously this early-returned, | |
| 1826 | + // leaving the old price_types JSON in place — the deletion silently | |
| 1827 | + // reverted on reload. Only the JSON is always cleared; the derived | |
| 1828 | + // scalar columns (original/discounted/sale) are reset solely for | |
| 1829 | + // traveler-based trips, because for regular pricing those columns | |
| 1830 | + // hold the actual price and must not be wiped. | |
| 1831 | + $table = TripsTable::getTableName(); | |
| 1832 | + $pricingType = $this->wpdb->get_var( | |
| 1833 | + $this->wpdb->prepare("SELECT pricing_type FROM `{$table}` WHERE id = %d", $tripId) | |
| 1834 | + ); | |
| 1835 | + | |
| 1836 | + $data = ['price_types' => null]; | |
| 1837 | + if ($pricingType === 'traveler_based') { | |
| 1838 | + $data['original_price'] = null; | |
| 1839 | + $data['discounted_price'] = null; | |
| 1840 | + $data['sale_price'] = null; | |
| 1841 | + } | |
| 1842 | + | |
| 1843 | + $this->wpdb->update($table, $data, ['id' => $tripId]); | |
| 1768 | 1844 | return; |
| 1769 | 1845 | } |
| 1770 | 1846 | |
| 1771 | 1847 | // Ensure at most one default category is set (keep the first truthy one). |
| @@ -2939,8 +3015,23 @@ | ||
| 2939 | 3015 | } |
| 2940 | 3016 | } |
| 2941 | 3017 | } |
| 2942 | 3018 | |
| 3019 | + // Fold in per-date / per-rule / per-departure price overrides so the | |
| 3020 | + // slider bounds span every price a customer can actually be charged, | |
| 3021 | + // not just the trips-table base. See collectPriceOverridePoints(). | |
| 3022 | + foreach ($this->collectPriceOverridePoints() as $price) { | |
| 3023 | + if ($price <= 0) { | |
| 3024 | + continue; | |
| 3025 | + } | |
| 3026 | + if ($minPrice === null || $price < $minPrice) { | |
| 3027 | + $minPrice = $price; | |
| 3028 | + } | |
| 3029 | + if ($maxPrice === null || $price > $maxPrice) { | |
| 3030 | + $maxPrice = $price; | |
| 3031 | + } | |
| 3032 | + } | |
| 3033 | + | |
| 2943 | 3034 | return (object) [ |
| 2944 | 3035 | 'min_price' => $minPrice, |
| 2945 | 3036 | 'max_price' => $maxPrice, |
| 2946 | 3037 | ]; |
| @@ -2993,8 +3084,111 @@ | ||
| 2993 | 3084 | return $prices; |
| 2994 | 3085 | } |
| 2995 | 3086 | |
| 2996 | 3087 | /** |
| 3088 | + * Effective price points that OVERRIDE a trip's base price for specific | |
| 3089 | + * dates, recurring rules, or individual departures. | |
| 3090 | + * | |
| 3091 | + * The price a customer actually sees, filters on, and pays is not always | |
| 3092 | + * the trips-table base price: it can be overridden per | |
| 3093 | + * - specific availability date (yatra_trip_availability_dates) | |
| 3094 | + * - recurring availability rule (yatra_trip_availability_rules) | |
| 3095 | + * - individual departure (yatra_trip_departures) | |
| 3096 | + * each carrying its own original/discounted price and per-category | |
| 3097 | + * (traveler-based) pricing JSON. The price-range slider bounds must span | |
| 3098 | + * these too — otherwise a trip whose highest bookable price lives only on | |
| 3099 | + * an override (e.g. a peak-season date priced €3,055 while the base is | |
| 3100 | + * lower) is unreachable on the filter even though customers can book it. | |
| 3101 | + * | |
| 3102 | + * Effective-price semantics mirror the customer-facing display exactly by | |
| 3103 | + * reusing collectTripDisplayPrices(): the discounted/sale price when set, | |
| 3104 | + * otherwise the original — the same amount shown and charged. Non-bookable | |
| 3105 | + * rows (blocked / cancelled / past dates, inactive rules) are excluded so | |
| 3106 | + * they can't push the slider max beyond any price a customer can reach. | |
| 3107 | + * | |
| 3108 | + * @return array<int,float> Effective override prices (each > 0). | |
| 3109 | + */ | |
| 3110 | + protected function collectPriceOverridePoints(): array | |
| 3111 | + { | |
| 3112 | + $prices = []; | |
| 3113 | + $today = current_time('Y-m-d'); | |
| 3114 | + | |
| 3115 | + // Only overrides that belong to a genuinely available (published, not | |
| 3116 | + // soft-deleted) trip may influence the bounds — otherwise a stray | |
| 3117 | + // override on a draft/trashed trip leaks a phantom max that no | |
| 3118 | + // customer-visible trip carries. Mirrors the trips-table filter the | |
| 3119 | + // base bound uses. | |
| 3120 | + $tripsTable = $this->getTableName(); | |
| 3121 | + $publishedJoin = "INNER JOIN {$tripsTable} t ON t.id = o.trip_id" | |
| 3122 | + . " AND t.status IN ('publish', 'published')" | |
| 3123 | + . " AND (t.deleted_at IS NULL OR t.deleted_at = '0000-00-00 00:00:00')"; | |
| 3124 | + | |
| 3125 | + // 1) Specific availability dates — same column shape as a trip row, so | |
| 3126 | + // collectTripDisplayPrices() resolves them identically. | |
| 3127 | + $datesTable = \Yatra\Database\Tables\TripAvailabilityDatesTable::getTableName(); | |
| 3128 | + $dateRows = $this->wpdb->get_results($this->wpdb->prepare( | |
| 3129 | + "SELECT o.original_price, o.discounted_price, NULL AS sale_price, o.price_types | |
| 3130 | + FROM {$datesTable} o | |
| 3131 | + {$publishedJoin} | |
| 3132 | + WHERE COALESCE(o.is_blocked, 0) = 0 | |
| 3133 | + AND o.status NOT IN ('cancelled', 'blocked', 'closed') | |
| 3134 | + AND (o.departure_date IS NULL OR o.departure_date >= %s)", | |
| 3135 | + $today | |
| 3136 | + )) ?: []; | |
| 3137 | + foreach ($dateRows as $row) { | |
| 3138 | + foreach ($this->collectTripDisplayPrices($row) as $p) { | |
| 3139 | + $prices[] = $p; | |
| 3140 | + } | |
| 3141 | + } | |
| 3142 | + | |
| 3143 | + // 2) Recurring rules (active only — mirrors the `status = 'active'` | |
| 3144 | + // filter RecurringAvailabilityRepository uses when generating | |
| 3145 | + // availability). `traveler_pricing` holds the per-category JSON; a | |
| 3146 | + // fixed `price_override` is an absolute per-booking price. A | |
| 3147 | + // percentage override is a relative adjustment to the trip base — | |
| 3148 | + // already spanned by the base bound — so it adds no new absolute max. | |
| 3149 | + $rulesTable = \Yatra\Database\Tables\TripAvailabilityRulesTable::getTableName(); | |
| 3150 | + $ruleRows = $this->wpdb->get_results( | |
| 3151 | + "SELECT o.original_price, o.sale_price, NULL AS discounted_price, | |
| 3152 | + o.price_override, o.price_type, o.traveler_pricing AS price_types | |
| 3153 | + FROM {$rulesTable} o | |
| 3154 | + {$publishedJoin} | |
| 3155 | + WHERE o.status = 'active'" | |
| 3156 | + ) ?: []; | |
| 3157 | + foreach ($ruleRows as $row) { | |
| 3158 | + foreach ($this->collectTripDisplayPrices($row) as $p) { | |
| 3159 | + $prices[] = $p; | |
| 3160 | + } | |
| 3161 | + if ($row->price_override !== null && (string) $row->price_type !== 'percentage') { | |
| 3162 | + $po = (float) $row->price_override; | |
| 3163 | + if ($po > 0) { | |
| 3164 | + $prices[] = $po; | |
| 3165 | + } | |
| 3166 | + } | |
| 3167 | + } | |
| 3168 | + | |
| 3169 | + // 3) Departures — `price_override` is an absolute price; | |
| 3170 | + // `price_by_traveler_type` is the per-category JSON. | |
| 3171 | + $depTable = \Yatra\Database\Tables\DeparturesTable::getTableName(); | |
| 3172 | + $depRows = $this->wpdb->get_results($this->wpdb->prepare( | |
| 3173 | + "SELECT o.price_override AS original_price, NULL AS discounted_price, | |
| 3174 | + NULL AS sale_price, o.price_by_traveler_type AS price_types | |
| 3175 | + FROM {$depTable} o | |
| 3176 | + {$publishedJoin} | |
| 3177 | + WHERE o.status NOT IN ('cancelled') | |
| 3178 | + AND (COALESCE(o.start_date, o.`date`) IS NULL OR COALESCE(o.start_date, o.`date`) >= %s)", | |
| 3179 | + $today | |
| 3180 | + )) ?: []; | |
| 3181 | + foreach ($depRows as $row) { | |
| 3182 | + foreach ($this->collectTripDisplayPrices($row) as $p) { | |
| 3183 | + $prices[] = $p; | |
| 3184 | + } | |
| 3185 | + } | |
| 3186 | + | |
| 3187 | + return $prices; | |
| 3188 | + } | |
| 3189 | + | |
| 3190 | + /** | |
| 2997 | 3191 | * Count trips by difficulty level |
| 2998 | 3192 | * |
| 2999 | 3193 | * @param int $difficultyLevelId Difficulty level ID |
| 3000 | 3194 | * @return int Number of trips with this difficulty level |
| @@ -3156,35 +3350,78 @@ | ||
| 3156 | 3350 | * Get price statistics for filter sidebar |
| 3157 | 3351 | */ |
| 3158 | 3352 | public function getPriceStats(): ?object |
| 3159 | 3353 | { |
| 3160 | - global $wpdb; | |
| 3161 | - | |
| 3162 | 3354 | $table = $this->getTableName(); |
| 3163 | - | |
| 3164 | - $result = $wpdb->get_row(" | |
| 3165 | - SELECT | |
| 3166 | - MIN(sub.eff_price) as min_price, | |
| 3167 | - MAX(sub.eff_price) as max_price, | |
| 3168 | - AVG(sub.eff_price) as avg_price | |
| 3169 | - FROM ( | |
| 3170 | - SELECT (CASE | |
| 3171 | - WHEN CAST(discounted_price AS DECIMAL(10,2)) > 0 THEN CAST(discounted_price AS DECIMAL(10,2)) | |
| 3172 | - WHEN CAST(sale_price AS DECIMAL(10,2)) > 0 THEN CAST(sale_price AS DECIMAL(10,2)) | |
| 3173 | - ELSE CAST(original_price AS DECIMAL(10,2)) | |
| 3174 | - END) AS eff_price | |
| 3175 | - FROM {$table} | |
| 3176 | - WHERE status IN ('publish', 'published') | |
| 3177 | - AND (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00') | |
| 3178 | - ) sub | |
| 3179 | - WHERE sub.eff_price > 0 | |
| 3180 | - "); | |
| 3181 | - | |
| 3182 | - return $result ? (object) [ | |
| 3183 | - 'min_price' => (float) $result->min_price, | |
| 3184 | - 'max_price' => (float) $result->max_price, | |
| 3185 | - 'avg_price' => (float) $result->avg_price | |
| 3186 | - ] : null; | |
| 3355 | + | |
| 3356 | + // Bounds must cover EVERY price a customer can actually filter on. | |
| 3357 | + // The search filter matches a trip if its legacy effective price OR | |
| 3358 | + // ANY per-category price in `price_types` falls in range (see the | |
| 3359 | + // price clause in the listing query), so the slider's min/max have to | |
| 3360 | + // span the same set — otherwise a traveler-based trip whose highest | |
| 3361 | + // tier (eg. Adult €3,055) lives only in `price_types` becomes | |
| 3362 | + // unreachable when the column-only max stops short (eg. €3,045). | |
| 3363 | + // | |
| 3364 | + // Reuse `collectTripDisplayPrices()` — the single source of truth also | |
| 3365 | + // used by `getPriceRangeStats()` and the listing/single-trip DISPLAY — | |
| 3366 | + // so bound == display == filter for both regular and traveler-based | |
| 3367 | + // pricing. | |
| 3368 | + $rows = $this->wpdb->get_results( | |
| 3369 | + "SELECT id, original_price, discounted_price, sale_price, price_types | |
| 3370 | + FROM {$table} | |
| 3371 | + WHERE status IN ('publish', 'published') | |
| 3372 | + AND (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00')" | |
| 3373 | + ) ?: []; | |
| 3374 | + | |
| 3375 | + $minPrice = null; | |
| 3376 | + $maxPrice = null; | |
| 3377 | + $sum = 0.0; | |
| 3378 | + $count = 0; | |
| 3379 | + | |
| 3380 | + foreach ($rows as $row) { | |
| 3381 | + foreach ($this->collectTripDisplayPrices($row) as $price) { | |
| 3382 | + if ($price <= 0) { | |
| 3383 | + continue; | |
| 3384 | + } | |
| 3385 | + if ($minPrice === null || $price < $minPrice) { | |
| 3386 | + $minPrice = $price; | |
| 3387 | + } | |
| 3388 | + if ($maxPrice === null || $price > $maxPrice) { | |
| 3389 | + $maxPrice = $price; | |
| 3390 | + } | |
| 3391 | + $sum += $price; | |
| 3392 | + $count++; | |
| 3393 | + } | |
| 3394 | + } | |
| 3395 | + | |
| 3396 | + // Fold in per-date / per-rule / per-departure price overrides so the | |
| 3397 | + // slider spans every price a customer can actually be charged (bounds | |
| 3398 | + // only — the average stays a base-trip figure). See | |
| 3399 | + // collectPriceOverridePoints(). | |
| 3400 | + foreach ($this->collectPriceOverridePoints() as $price) { | |
| 3401 | + if ($price <= 0) { | |
| 3402 | + continue; | |
| 3403 | + } | |
| 3404 | + if ($minPrice === null || $price < $minPrice) { | |
| 3405 | + $minPrice = $price; | |
| 3406 | + } | |
| 3407 | + if ($maxPrice === null || $price > $maxPrice) { | |
| 3408 | + $maxPrice = $price; | |
| 3409 | + } | |
| 3410 | + } | |
| 3411 | + | |
| 3412 | + if ($minPrice === null || $maxPrice === null) { | |
| 3413 | + return null; | |
| 3414 | + } | |
| 3415 | + | |
| 3416 | + // Widen to whole units so the template's `(int)` cast on the bounds | |
| 3417 | + // can never truncate a fractional boundary out of range (eg. a | |
| 3418 | + // €3,055.50 max would otherwise int-cast to 3055 and exclude it). | |
| 3419 | + return (object) [ | |
| 3420 | + 'min_price' => (float) floor($minPrice), | |
| 3421 | + 'max_price' => (float) ceil($maxPrice), | |
| 3422 | + 'avg_price' => $count > 0 ? (float) ($sum / $count) : 0.0, | |
| 3423 | + ]; | |
| 3187 | 3424 | } |
| 3188 | 3425 | |
| 3189 | 3426 | /** |
| 3190 | 3427 | * Distinct accommodation_type values on published trips with counts. |
| @@ -3494,16 +3731,48 @@ | ||
| 3494 | 3731 | return 0; |
| 3495 | 3732 | } |
| 3496 | 3733 | |
| 3497 | 3734 | /** |
| 3735 | + * Age thresholds behind the suitability filters. | |
| 3736 | + * | |
| 3737 | + * These were repeated as literals in six places — the filter query and the | |
| 3738 | + * count query for each band — so an operator whose "kids" means under 18 | |
| 3739 | + * rather than under 12 had no way to say so, and changing it meant editing | |
| 3740 | + * the same number in six spots and hoping none were missed. | |
| 3741 | + * | |
| 3742 | + * @return array{family_max:int, kids_max:int, senior_min:int, adults_min:int} | |
| 3743 | + */ | |
| 3744 | + public static function ageSuitabilityThresholds(): array | |
| 3745 | + { | |
| 3746 | + $defaults = [ | |
| 3747 | + 'family_max' => 5, | |
| 3748 | + 'kids_max' => 12, | |
| 3749 | + 'senior_min' => 65, | |
| 3750 | + 'adults_min' => 18, | |
| 3751 | + ]; | |
| 3752 | + | |
| 3753 | + $filtered = (array) apply_filters('yatra_age_suitability_thresholds', $defaults); | |
| 3754 | + | |
| 3755 | + foreach ($defaults as $key => $fallback) { | |
| 3756 | + $filtered[$key] = isset($filtered[$key]) && is_numeric($filtered[$key]) | |
| 3757 | + ? (int) $filtered[$key] | |
| 3758 | + : $fallback; | |
| 3759 | + } | |
| 3760 | + | |
| 3761 | + return $filtered; | |
| 3762 | + } | |
| 3763 | + | |
| 3764 | + /** | |
| 3498 | 3765 | * Count family friendly trips |
| 3499 | 3766 | */ |
| 3500 | 3767 | public function countByFamilyFriendly(): int |
| 3501 | 3768 | { |
| 3502 | 3769 | $table = $this->getTableName(); |
| 3770 | + $limit = (int) self::ageSuitabilityThresholds()['family_max']; | |
| 3771 | + | |
| 3503 | 3772 | return (int) $this->wpdb->get_var( |
| 3504 | - "SELECT COUNT(*) FROM {$table} | |
| 3505 | - WHERE status = 'publish' AND (age_min IS NULL OR age_min <= 5)" | |
| 3773 | + "SELECT COUNT(*) FROM {$table} | |
| 3774 | + WHERE status = 'publish' AND (age_min IS NULL OR age_min <= {$limit})" | |
| 3506 | 3775 | ); |
| 3507 | 3776 | } |
| 3508 | 3777 | |
| 3509 | 3778 | /** |
| @@ -3511,11 +3780,13 @@ | ||
| 3511 | 3780 | */ |
| 3512 | 3781 | public function countByKidsFriendly(): int |
| 3513 | 3782 | { |
| 3514 | 3783 | $table = $this->getTableName(); |
| 3784 | + $limit = (int) self::ageSuitabilityThresholds()['kids_max']; | |
| 3785 | + | |
| 3515 | 3786 | return (int) $this->wpdb->get_var( |
| 3516 | - "SELECT COUNT(*) FROM {$table} | |
| 3517 | - WHERE status = 'publish' AND (age_min IS NULL OR age_min <= 12)" | |
| 3787 | + "SELECT COUNT(*) FROM {$table} | |
| 3788 | + WHERE status = 'publish' AND (age_min IS NULL OR age_min <= {$limit})" | |
| 3518 | 3789 | ); |
| 3519 | 3790 | } |
| 3520 | 3791 | |
| 3521 | 3792 | /** |
| @@ -3523,11 +3794,13 @@ | ||
| 3523 | 3794 | */ |
| 3524 | 3795 | public function countBySeniorFriendly(): int |
| 3525 | 3796 | { |
| 3526 | 3797 | $table = $this->getTableName(); |
| 3798 | + $limit = (int) self::ageSuitabilityThresholds()['senior_min']; | |
| 3799 | + | |
| 3527 | 3800 | return (int) $this->wpdb->get_var( |
| 3528 | - "SELECT COUNT(*) FROM {$table} | |
| 3529 | - WHERE status = 'publish' AND (age_max IS NULL OR age_max >= 65)" | |
| 3801 | + "SELECT COUNT(*) FROM {$table} | |
| 3802 | + WHERE status = 'publish' AND (age_max IS NULL OR age_max >= {$limit})" | |
| 3530 | 3803 | ); |
| 3531 | 3804 | } |
| 3532 | 3805 | |
| 3533 | 3806 | /** |
| @@ -3535,11 +3808,13 @@ | ||
| 3535 | 3808 | */ |
| 3536 | 3809 | public function countByAdultsOnly(): int |
| 3537 | 3810 | { |
| 3538 | 3811 | $table = $this->getTableName(); |
| 3812 | + $limit = (int) self::ageSuitabilityThresholds()['adults_min']; | |
| 3813 | + | |
| 3539 | 3814 | return (int) $this->wpdb->get_var( |
| 3540 | - "SELECT COUNT(*) FROM {$table} | |
| 3541 | - WHERE status = 'publish' AND age_min >= 18" | |
| 3815 | + "SELECT COUNT(*) FROM {$table} | |
| 3816 | + WHERE status = 'publish' AND age_min >= {$limit}" | |
| 3542 | 3817 | ); |
| 3543 | 3818 | } |
| 3544 | 3819 | |
| 3545 | 3820 | /** |