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 +126 -34 3.0.2.8 → 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 {
@@ -1004,80 +1026,127 @@
1004 1026 ];
1005 1027 }
1006 1028
1007 1029 /**
1008 - * 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}
1009 1036 */
1010 - public function getDifficulty(): array
1011 - {
1037 + public static function resolveDifficultyDisplay(
1038 + ?string $difficultyLevel,
1039 + ?string $difficultyName = null,
1040 + $difficultyIcon = null
1041 + ): array {
1012 1042 $difficulty = '';
1013 1043 $difficulty_icon = '';
1014 -
1015 - // Always prioritize fetching from difficulty_levels table if we have a difficulty_level ID
1016 - // Check if difficulty_level is set and is a valid positive integer (not 0, null, or empty)
1017 - 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) {
1018 1048 global $wpdb;
1019 -
1049 +
1020 1050 $difficulty_data = $wpdb->get_row($wpdb->prepare(
1021 - "SELECT * FROM " . ClassificationsTable::getTableName() . " WHERE id = %d AND type = %s",
1022 - (int) $this->difficulty_level,
1051 + 'SELECT * FROM ' . ClassificationsTable::getTableName() . ' WHERE id = %d AND type = %s',
1052 + (int) $difficultyLevel,
1023 1053 ClassificationTypes::DIFFICULTY
1024 1054 ));
1025 -
1055 +
1026 1056 if ($difficulty_data) {
1027 - $difficulty = $difficulty_data->name;
1057 + $difficulty = (string) $difficulty_data->name;
1028 1058 if (!empty($difficulty_data->icon)) {
1029 1059 $icon_data = maybe_unserialize($difficulty_data->icon);
1030 - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1031 - $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 + }
1032 1065 } elseif (is_string($difficulty_data->icon)) {
1033 1066 $difficulty_icon = $difficulty_data->icon;
1067 + $icon_picker = [
1068 + 'type' => 'icon',
1069 + 'value' => $difficulty_icon,
1070 + 'provider' => 'yatra',
1071 + ];
1034 1072 }
1035 1073 }
1036 1074 }
1037 1075 }
1038 -
1039 - // Fallback: Use pre-populated difficulty_name from main query (AppServiceProvider)
1040 - if (empty($difficulty) && !empty($this->difficulty_name)) {
1041 - $difficulty = $this->difficulty_name;
1076 +
1077 + if ($difficulty === '' && !empty($difficultyName)) {
1078 + $difficulty = $difficultyName;
1042 1079 }
1043 -
1044 - // Fallback: Use pre-populated difficulty_icon from main query (AppServiceProvider)
1045 - if (empty($difficulty_icon) && !empty($this->difficulty_icon)) {
1046 - $icon_data = maybe_unserialize($this->difficulty_icon);
1047 - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
1048 - $difficulty_icon = $icon_data['value'];
1049 - } elseif (is_string($this->difficulty_icon)) {
1050 - $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 + ];
1051 1100 }
1052 1101 }
1053 -
1054 - // If we have difficulty but no icon, provide a default icon
1055 - if (!empty($difficulty) && empty($difficulty_icon)) {
1102 +
1103 + if ($difficulty !== '' && $icon_picker === null && $difficulty_icon === '') {
1056 1104 $difficulty_icon = 'mountain';
1105 + $icon_picker = [
1106 + 'type' => 'icon',
1107 + 'value' => 'mountain',
1108 + 'provider' => 'yatra',
1109 + ];
1057 1110 }
1058 -
1111 +
1059 1112 return [
1060 1113 'level' => $difficulty,
1061 1114 'icon' => $difficulty_icon,
1062 - 'has_difficulty' => !empty($difficulty)
1115 + 'icon_picker' => $icon_picker,
1116 + 'has_difficulty' => $difficulty !== '',
1063 1117 ];
1064 1118 }
1065 1119
1066 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 + /**
1067 1133 * Get trip duration information
1068 1134 */
1069 1135 public function getDuration(): array
1070 1136 {
1071 1137 $duration = '';
1072 - if (!empty($this->duration_days)) {
1138 + if ($this->isHoursBased()) {
1139 + $duration = yatra_format_duration(0, null, $this->getDurationHours());
1140 + } elseif (!empty($this->duration_days)) {
1073 1141 $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null);
1074 1142 }
1075 -
1143 +
1076 1144 return [
1077 1145 'formatted' => $duration,
1078 1146 'days' => $this->duration_days ?? 0,
1079 1147 'nights' => $this->duration_nights ?? 0,
1148 + 'hours' => $this->getDurationHours(),
1080 1149 'has_duration' => !empty($duration)
1081 1150 ];
1082 1151 }
1083 1152
@@ -1497,9 +1566,32 @@
1497 1566 return $this->duration_nights ?? 0;
1498 1567 }
1499 1568
1500 1569 /**
1501 - * 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()}.
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.
1502 1594 */
1503 1595 public function getDifficultyLevel(): string
1504 1596 {
1505 1597 return $this->difficulty_level ?? '';