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 | app/Models/Trip.php +164 -41 3.0.2.6 → 3.0.16 View file →
@@ -233,8 +233,17 @@
233 233 public array $tags = [];
234 234 public array $included_items = [];
235 235 public array $excluded_items = [];
236 236 public array $gallery_images = [];
237 + // Media arrays computed in SingleTripController::prepareTrip(). These MUST be
238 + // declared so Trip::fromStdClass() (which only copies declared properties via
239 + // property_exists) carries them onto the model — otherwise the single-trip
240 + // hero media switcher reads empty arrays and the Video / 360° / Documents
241 + // buttons never render even when video_url / virtual_tour_url are set.
242 + public array $videos = [];
243 + public array $youtube_videos = [];
244 + public array $virtual_tours = [];
245 + public array $documents = [];
237 246 public array $price_types = [];
238 247 public array $itinerary_days = [];
239 248 public array $faqs = [];
240 249 public array $frontend_tabs = [];
@@ -654,8 +663,21 @@
654 663 ];
655 664 }
656 665
657 666 /**
667 + * Returns true when the trip is set to enquiry-only (booking calendar disabled).
668 + * Stored as custom_fields['disable_booking'] = true. Requires Pro to be active.
669 + */
670 + public function isBookingDisabled(): bool
671 + {
672 + if (!apply_filters('yatra_is_pro_active', false)) {
673 + return false;
674 + }
675 +
676 + return !empty($this->custom_fields['disable_booking']);
677 + }
678 +
679 + /**
658 680 * Parse JSON field from database
659 681 */
660 682 private static function parseJsonField(?string $value, array $default = []): array
661 683 {
@@ -869,11 +891,14 @@
869 891 $current_price_raw = 0;
870 892 $original_price_raw = 0;
871 893
872 894 if ($is_traveler_based) {
873 - // For traveler-based pricing, find minimum price from all categories
895 + // For traveler-based pricing, prefer admin-selected default category if present;
896 + // otherwise fall back to minimum across categories (legacy behavior).
897 + $default_price = 0.0;
898 + $default_original_price = 0.0;
874 899 $min_price = PHP_FLOAT_MAX;
875 - $min_original_price = 0;
900 + $min_original_price = 0.0;
876 901
877 902 // Check multiple possible data structures for price categories
878 903 $price_categories = [];
879 904
@@ -892,12 +917,14 @@
892 917 if (is_object($category)) {
893 918 $regular_price = (float) ($category->original_price ?? $category->price ?? 0);
894 919 // Priority: discounted_price > sale_price (legacy) > discount_price (legacy)
895 920 $discounted_price = (float) ($category->discounted_price ?? $category->sale_price ?? $category->discount_price ?? 0);
921 + $is_default = !empty($category->is_default);
896 922 } elseif (is_array($category)) {
897 923 $regular_price = (float) ($category['original_price'] ?? $category['price'] ?? 0);
898 924 // Priority: discounted_price > sale_price (legacy) > discount_price (legacy)
899 925 $discounted_price = (float) ($category['discounted_price'] ?? $category['sale_price'] ?? $category['discount_price'] ?? 0);
926 + $is_default = !empty($category['is_default']);
900 927 } else {
901 928 continue;
902 929 }
903 930
@@ -902,8 +929,16 @@
902 929 }
903 930
904 931 // Use discounted price if available, otherwise regular price
905 932 $category_price = ($discounted_price > 0 && $discounted_price < $regular_price) ? $discounted_price : $regular_price;
933 +
934 + // Capture default category price (first valid default only)
935 + if ($is_default && $default_price <= 0 && $category_price > 0) {
936 + $default_price = $category_price;
937 + if ($discounted_price > 0 && $discounted_price < $regular_price) {
938 + $default_original_price = $regular_price;
939 + }
940 + }
906 941
907 942 if ($category_price > 0 && $category_price < $min_price) {
908 943 $min_price = $category_price;
909 944 // Store original price from the same category for strikethrough
@@ -929,16 +964,25 @@
929 964 $min_price = (float) $this->starting_price;
930 965 }
931 966 }
932 967
933 - if ($min_price !== PHP_FLOAT_MAX && $min_price > 0) {
934 - $current_price_raw = $min_price;
968 + $chosen_price = $default_price > 0 ? $default_price : $min_price;
969 + $chosen_original = $default_price > 0 ? $default_original_price : $min_original_price;
970 + $chosen_has_discount = $default_price > 0
971 + ? ($default_original_price > 0 && $default_original_price > $default_price)
972 + : $has_discount;
973 +
974 + if ($chosen_price !== PHP_FLOAT_MAX && $chosen_price > 0) {
975 + $current_price_raw = $chosen_price;
935 976 $current_price = yatra_format_price($current_price_raw);
936 977 $price_prefix = __('From ', 'yatra'); // Always show "From" for traveler-based
937 978
938 - if ($has_discount && $min_original_price > 0) {
939 - $original_price_raw = $min_original_price;
979 + if ($chosen_has_discount && $chosen_original > 0) {
980 + $original_price_raw = $chosen_original;
940 981 $original_price = yatra_format_price($original_price_raw);
982 + $has_discount = true;
983 + } else {
984 + $has_discount = false;
941 985 }
942 986 }
943 987 } else {
944 988 // Regular pricing - NO "From" text
@@ -982,80 +1026,127 @@
982 1026 ];
983 1027 }
984 1028
985 1029 /**
986 - * Get difficulty information
1030 + * Resolve difficulty label + icon for display (shared by single trip, similar trips, etc.).
1031 + *
1032 + * @param string|null $difficultyLevel Raw trip.difficulty_level (usually classification row id).
1033 + * @param string|null $difficultyName Optional joined name from list queries.
1034 + * @param mixed|null $difficultyIcon Optional serialized icon payload from list queries.
1035 + * @return array{level: string, icon: string, icon_picker: array<string, mixed>|null, has_difficulty: bool}
987 1036 */
988 - public function getDifficulty(): array
989 - {
1037 + public static function resolveDifficultyDisplay(
1038 + ?string $difficultyLevel,
1039 + ?string $difficultyName = null,
1040 + $difficultyIcon = null
1041 + ): array {
990 1042 $difficulty = '';
991 1043 $difficulty_icon = '';
992 -
993 - // Always prioritize fetching from difficulty_levels table if we have a difficulty_level ID
994 - // Check if difficulty_level is set and is a valid positive integer (not 0, null, or empty)
995 - if (!empty($this->difficulty_level) && is_numeric($this->difficulty_level) && (int) $this->difficulty_level > 0) {
1044 + /** @var array<string, mixed>|null $icon_picker */
1045 + $icon_picker = null;
1046 +
1047 + if (!empty($difficultyLevel) && is_numeric($difficultyLevel) && (int) $difficultyLevel > 0) {
996 1048 global $wpdb;
997 -
1049 +
998 1050 $difficulty_data = $wpdb->get_row($wpdb->prepare(
999 - "SELECT * FROM " . ClassificationsTable::getTableName() . " WHERE id = %d AND type = %s",
1000 - (int) $this->difficulty_level,
1051 + 'SELECT * FROM ' . ClassificationsTable::getTableName() . ' WHERE id = %d AND type = %s',
1052 + (int) $difficultyLevel,
1001 1053 ClassificationTypes::DIFFICULTY
1002 1054 ));
1003 -
1055 +
1004 1056 if ($difficulty_data) {
1005 - $difficulty = $difficulty_data->name;
1057 + $difficulty = (string) $difficulty_data->name;
1006 1058 if (!empty($difficulty_data->icon)) {
1007 1059 $icon_data = maybe_unserialize($difficulty_data->icon);
1008 - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1009 - $difficulty_icon = $icon_data['value'];
1060 + if (is_array($icon_data) && isset($icon_data['type'])) {
1061 + $icon_picker = $icon_data;
1062 + if ($icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1063 + $difficulty_icon = (string) $icon_data['value'];
1064 + }
1010 1065 } elseif (is_string($difficulty_data->icon)) {
1011 1066 $difficulty_icon = $difficulty_data->icon;
1067 + $icon_picker = [
1068 + 'type' => 'icon',
1069 + 'value' => $difficulty_icon,
1070 + 'provider' => 'yatra',
1071 + ];
1012 1072 }
1013 1073 }
1014 1074 }
1015 1075 }
1016 -
1017 - // Fallback: Use pre-populated difficulty_name from main query (AppServiceProvider)
1018 - if (empty($difficulty) && !empty($this->difficulty_name)) {
1019 - $difficulty = $this->difficulty_name;
1076 +
1077 + if ($difficulty === '' && !empty($difficultyName)) {
1078 + $difficulty = $difficultyName;
1020 1079 }
1021 -
1022 - // Fallback: Use pre-populated difficulty_icon from main query (AppServiceProvider)
1023 - if (empty($difficulty_icon) && !empty($this->difficulty_icon)) {
1024 - $icon_data = maybe_unserialize($this->difficulty_icon);
1025 - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1026 - $difficulty_icon = $icon_data['value'];
1027 - } elseif (is_string($this->difficulty_icon)) {
1028 - $difficulty_icon = $this->difficulty_icon;
1080 +
1081 + // Legacy / non-id values stored in difficulty_level (e.g. slug text).
1082 + if ($difficulty === '' && $difficultyLevel !== null && $difficultyLevel !== '' && !is_numeric($difficultyLevel)) {
1083 + $difficulty = ucfirst($difficultyLevel);
1084 + }
1085 +
1086 + if ($icon_picker === null && $difficultyIcon !== null && $difficultyIcon !== '') {
1087 + $icon_data = is_string($difficultyIcon) ? maybe_unserialize($difficultyIcon) : $difficultyIcon;
1088 + if (is_array($icon_data) && isset($icon_data['type'])) {
1089 + $icon_picker = $icon_data;
1090 + if ($icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1091 + $difficulty_icon = (string) $icon_data['value'];
1092 + }
1093 + } elseif (is_string($difficultyIcon)) {
1094 + $difficulty_icon = $difficultyIcon;
1095 + $icon_picker = [
1096 + 'type' => 'icon',
1097 + 'value' => $difficulty_icon,
1098 + 'provider' => 'yatra',
1099 + ];
1029 1100 }
1030 1101 }
1031 -
1032 - // If we have difficulty but no icon, provide a default icon
1033 - if (!empty($difficulty) && empty($difficulty_icon)) {
1102 +
1103 + if ($difficulty !== '' && $icon_picker === null && $difficulty_icon === '') {
1034 1104 $difficulty_icon = 'mountain';
1105 + $icon_picker = [
1106 + 'type' => 'icon',
1107 + 'value' => 'mountain',
1108 + 'provider' => 'yatra',
1109 + ];
1035 1110 }
1036 -
1111 +
1037 1112 return [
1038 1113 'level' => $difficulty,
1039 1114 'icon' => $difficulty_icon,
1040 - 'has_difficulty' => !empty($difficulty)
1115 + 'icon_picker' => $icon_picker,
1116 + 'has_difficulty' => $difficulty !== '',
1041 1117 ];
1042 1118 }
1043 1119
1044 1120 /**
1121 + * Get difficulty information
1122 + */
1123 + public function getDifficulty(): array
1124 + {
1125 + return self::resolveDifficultyDisplay(
1126 + $this->difficulty_level,
1127 + $this->difficulty_name,
1128 + $this->difficulty_icon
1129 + );
1130 + }
1131 +
1132 + /**
1045 1133 * Get trip duration information
1046 1134 */
1047 1135 public function getDuration(): array
1048 1136 {
1049 1137 $duration = '';
1050 - if (!empty($this->duration_days)) {
1138 + if ($this->isHoursBased()) {
1139 + $duration = yatra_format_duration(0, null, $this->getDurationHours());
1140 + } elseif (!empty($this->duration_days)) {
1051 1141 $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null);
1052 1142 }
1053 -
1143 +
1054 1144 return [
1055 1145 'formatted' => $duration,
1056 1146 'days' => $this->duration_days ?? 0,
1057 1147 'nights' => $this->duration_nights ?? 0,
1148 + 'hours' => $this->getDurationHours(),
1058 1149 'has_duration' => !empty($duration)
1059 1150 ];
1060 1151 }
1061 1152
@@ -1183,9 +1274,10 @@
1183 1274 */
1184 1275 public function getEffectivePrice(): float
1185 1276 {
1186 1277 if ($this->pricing_type === 'traveler_based') {
1187 - return $this->min_category_original_price ?? 0.0;
1278 + // Use centralized logic (default category if set, else minimum).
1279 + return (float) \Yatra\Services\TripPricingService::getEffectivePrice((object) $this);
1188 1280 }
1189 1281
1190 1282 // Use discounted price if available and valid
1191 1283 if (!empty($this->discounted_price) && $this->discounted_price < $this->original_price) {
@@ -1474,10 +1566,33 @@
1474 1566 return $this->duration_nights ?? 0;
1475 1567 }
1476 1568
1477 1569 /**
1478 - * Get difficulty level
1570 + * Duration in hours for hour-based (single-day) tours. 0 when the tour is
1571 + * day-based (the default) — see {@see self::isHoursBased()}.
1479 1572 */
1573 + public function getDurationHours(): int
1574 + {
1575 + return (int) ($this->duration_hours ?? 0);
1576 + }
1577 +
1578 + /**
1579 + * Whether this tour's duration is expressed in hours rather than days.
1580 + *
1581 + * True only when a positive `duration_hours` is set — so every existing
1582 + * (day-based) trip returns false and keeps its current behaviour. Used by
1583 + * the front-end duration display and the Google Calendar module to build a
1584 + * same-day timed event instead of a multi-day span.
1585 + */
1586 + public function isHoursBased(): bool
1587 + {
1588 + return $this->getDurationHours() > 0;
1589 + }
1590 +
1591 + /**
1592 + * Raw difficulty classification ID on the trip row (not a human-readable label).
1593 + * For display, use {@see self::getDifficulty()} which resolves the name from the classifications table.
1594 + */
1480 1595 public function getDifficultyLevel(): string
1481 1596 {
1482 1597 return $this->difficulty_level ?? '';
1483 1598 }
@@ -1502,8 +1617,12 @@
1502 1617 * Get original price
1503 1618 */
1504 1619 public function getOriginalPrice(): float
1505 1620 {
1621 + if ($this->pricing_type === 'traveler_based') {
1622 + $pricing = \Yatra\Services\TripPricingService::resolveDisplayPricing((object) $this);
1623 + return (float) ($pricing['min_category_original_price'] ?? 0);
1624 + }
1506 1625 return (float) ($this->original_price ?? 0);
1507 1626 }
1508 1627
1509 1628 /**
@@ -1510,8 +1629,12 @@
1510 1629 * Get sale price
1511 1630 */
1512 1631 public function getSalePrice(): float
1513 1632 {
1633 + if ($this->pricing_type === 'traveler_based') {
1634 + $pricing = \Yatra\Services\TripPricingService::resolveDisplayPricing((object) $this);
1635 + return (float) ($pricing['effective_price_min'] ?? 0);
1636 + }
1514 1637 return (float) ($this->sale_price ?? 0);
1515 1638 }
1516 1639
1517 1640 /**