| @@ -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']; |
| @@ -3675,16 +3731,48 @@ | ||
| 3675 | 3731 | return 0; |
| 3676 | 3732 | } |
| 3677 | 3733 | |
| 3678 | 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 | + /** | |
| 3679 | 3765 | * Count family friendly trips |
| 3680 | 3766 | */ |
| 3681 | 3767 | public function countByFamilyFriendly(): int |
| 3682 | 3768 | { |
| 3683 | 3769 | $table = $this->getTableName(); |
| 3770 | + $limit = (int) self::ageSuitabilityThresholds()['family_max']; | |
| 3771 | + | |
| 3684 | 3772 | return (int) $this->wpdb->get_var( |
| 3685 | - "SELECT COUNT(*) FROM {$table} | |
| 3686 | - 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})" | |
| 3687 | 3775 | ); |
| 3688 | 3776 | } |
| 3689 | 3777 | |
| 3690 | 3778 | /** |
| @@ -3692,11 +3780,13 @@ | ||
| 3692 | 3780 | */ |
| 3693 | 3781 | public function countByKidsFriendly(): int |
| 3694 | 3782 | { |
| 3695 | 3783 | $table = $this->getTableName(); |
| 3784 | + $limit = (int) self::ageSuitabilityThresholds()['kids_max']; | |
| 3785 | + | |
| 3696 | 3786 | return (int) $this->wpdb->get_var( |
| 3697 | - "SELECT COUNT(*) FROM {$table} | |
| 3698 | - 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})" | |
| 3699 | 3789 | ); |
| 3700 | 3790 | } |
| 3701 | 3791 | |
| 3702 | 3792 | /** |
| @@ -3704,11 +3794,13 @@ | ||
| 3704 | 3794 | */ |
| 3705 | 3795 | public function countBySeniorFriendly(): int |
| 3706 | 3796 | { |
| 3707 | 3797 | $table = $this->getTableName(); |
| 3798 | + $limit = (int) self::ageSuitabilityThresholds()['senior_min']; | |
| 3799 | + | |
| 3708 | 3800 | return (int) $this->wpdb->get_var( |
| 3709 | - "SELECT COUNT(*) FROM {$table} | |
| 3710 | - 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})" | |
| 3711 | 3803 | ); |
| 3712 | 3804 | } |
| 3713 | 3805 | |
| 3714 | 3806 | /** |
| @@ -3716,11 +3808,13 @@ | ||
| 3716 | 3808 | */ |
| 3717 | 3809 | public function countByAdultsOnly(): int |
| 3718 | 3810 | { |
| 3719 | 3811 | $table = $this->getTableName(); |
| 3812 | + $limit = (int) self::ageSuitabilityThresholds()['adults_min']; | |
| 3813 | + | |
| 3720 | 3814 | return (int) $this->wpdb->get_var( |
| 3721 | - "SELECT COUNT(*) FROM {$table} | |
| 3722 | - WHERE status = 'publish' AND age_min >= 18" | |
| 3815 | + "SELECT COUNT(*) FROM {$table} | |
| 3816 | + WHERE status = 'publish' AND age_min >= {$limit}" | |
| 3723 | 3817 | ); |
| 3724 | 3818 | } |
| 3725 | 3819 | |
| 3726 | 3820 | /** |