PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / app / Models / Trip.php

Trip.php in Yatra – Travel Booking & Tour Operator Software 3.0.15, at app/Models/Trip.php

1,975 lines 74.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Models;
6
7 use Yatra\Constants\ClassificationTypes;
8 use Yatra\Database\Tables\ClassificationsTable;
9
10 /**
11 * Trip Model
12 * Represents a trip/tour entity with comprehensive data structure
13 *
14 * Expert-level model design:
15 * - Type-safe properties
16 * - Proper data serialization/deserialization
17 * - JSON field handling
18 * - Relationship data loading
19 */
20 class Trip
21 {
22 // Identification & Basic Info
23 public int $id = 0;
24 public string $title = '';
25 public string $slug = '';
26 public ?string $trip_code = null;
27 public string $description = '';
28 public ?string $short_description = null;
29 public string $trip_details = '';
30 public string $what_makes_special = '';
31 public string $trip_story = '';
32 public ?string $excerpt = null;
33
34 // Location & Geography
35 public ?string $starting_location = null;
36 public ?string $ending_location = null;
37 public ?string $latitude = null;
38 public ?string $longitude = null;
39 public ?string $starting_latitude = null;
40 public ?string $starting_longitude = null;
41 public ?string $ending_latitude = null;
42 public ?string $ending_longitude = null;
43 public int $map_zoom_level = 10;
44 public ?string $timezone = null;
45 public ?string $country_code = null;
46
47 // Duration & Schedule
48 public string $trip_type = 'multi_day';
49 public ?int $duration_days = null;
50 public ?int $duration_nights = null;
51 public ?int $duration_hours = null;
52 public ?string $available_from = null;
53 public ?string $available_to = null;
54 public int $booking_window_days = 30;
55 public int $booking_deadline_hours = 24;
56 public bool $flexible_dates = false;
57 public bool $fixed_departures_only = false;
58
59 // Seasonal & Availability
60 public ?string $seasonal_availability = null;
61 public ?string $best_season = null;
62 public ?string $peak_season = null;
63 public ?string $off_season = null;
64 public bool $seasonal_auto_enable = false;
65 public ?string $seasonal_enable_date = null;
66 public ?string $seasonal_disable_date = null;
67 public array $blackout_dates = [];
68
69 // Booking Mode & Availability Flags
70 public string $booking_mode = 'flexible'; // 'date_specific' or 'flexible'
71 public bool $has_specific_availability = false; // Has configured availability dates/rules
72 public bool $has_availability = false; // Backward compatibility: has specific dates in current range
73 public bool $has_booking_capability = true; // Can be booked (always true for good UX)
74
75 // Fallback Settings (for trips without availability dates/rules)
76 public bool $has_default_time_slots = false;
77 public ?string $default_time_slots = null; // JSON string
78 public ?string $departure_time = null;
79
80 // Categorization
81 public ?string $trip_category = null;
82 public ?string $trip_category_parent = null;
83 public ?string $trip_category_sub = null;
84 public ?string $difficulty_level = null;
85 public ?string $difficulty_name = null;
86 public ?string $difficulty_icon = null;
87 public ?string $activity_intensity = null;
88 public string $featured_priority = 'none';
89 public ?string $trip_style = null;
90 public string $group_type = 'both';
91
92 // Pricing
93 public string $pricing_type = 'regular';
94 public float $original_price = 0.00;
95 public ?float $discounted_price = null;
96 public ?float $sale_price = null;
97 public ?float $effective_price_min = null;
98 public ?float $min_category_original_price = null;
99 public ?int $max_discount_percentage = null;
100 public string $currency = 'USD';
101 public bool $price_per_person = true;
102
103 // Discount-specific properties for discount shortcode
104 public bool $has_discount = false;
105 public ?array $best_discount = null;
106 public ?float $current_price = null;
107 public ?float $discount_percentage = null;
108 public bool $deposit_required = false;
109 public ?float $deposit_amount = null;
110 public ?float $deposit_percentage = null;
111 public string $payment_terms = '';
112 public bool $payment_plans_enabled = false;
113 public bool $tax_included = false;
114 public ?float $tax_rate = null;
115 public ?float $service_charge = null;
116 public ?float $service_charge_percentage = null;
117
118 // Group Pricing
119 public bool $group_pricing_enabled = false;
120 public ?int $group_size_min = null;
121 public ?int $group_size_max = null;
122 public string $group_discount_type = 'percentage';
123 public ?float $group_discount_percentage = null;
124 public ?float $group_discount_amount = null;
125
126 // Early Bird / Last Minute
127 public bool $early_bird_discount_enabled = false;
128 public ?int $early_bird_days = null;
129 public ?float $early_bird_discount = null;
130 public bool $last_minute_discount_enabled = false;
131 public ?int $last_minute_days = null;
132 public ?float $last_minute_discount = null;
133
134 // Booking Settings
135 public int $min_travelers = 1;
136 public ?int $max_travelers = null;
137 public ?int $max_travelers_per_booking = null;
138 public bool $waitlist_enabled = false;
139 public ?int $waitlist_capacity = null;
140 public bool $instant_booking = true;
141 public bool $requires_approval = false;
142 public bool $booking_confirmation_email = true;
143 public bool $booking_reminder_email = true;
144 public int $reminder_days_before = 7;
145
146 // Requirements
147 public ?int $age_min = null;
148 public ?int $age_max = null;
149 public string $physical_requirements = '';
150 public string $medical_requirements = '';
151 public string $visa_requirements = '';
152 public string $vaccination_requirements = '';
153 public int $passport_validity_months = 6;
154 public bool $travel_insurance_required = false;
155 public string $special_equipment = '';
156
157 // Policies
158 public string $cancellation_policy = '';
159 public string $refund_policy = '';
160 public string $change_policy = '';
161 public string $weather_policy = '';
162 public string $force_majeure_policy = '';
163 public string $terms_conditions = '';
164
165 // Accommodation
166 public ?string $accommodation_type = null;
167 public ?string $accommodation_standard = null;
168 public ?string $meal_plan = null;
169 public string $accommodation_details = '';
170 public bool $accommodation_included = true;
171
172 // Transportation
173 public bool $transportation_included = false;
174 public ?string $pickup_location = null;
175 public ?string $pickup_location_lat = null;
176 public ?string $pickup_location_lng = null;
177 public ?string $dropoff_location = null;
178 public ?string $dropoff_location_lat = null;
179 public ?string $dropoff_location_lng = null;
180 public string $transportation_details = '';
181 public string $internal_transportation = '';
182 public bool $international_flights_included = false;
183 public bool $domestic_flights_included = false;
184
185 // Media
186 public ?int $featured_image = null;
187 public ?string $featured_image_url = null;
188 public ?string $video_url = null;
189 public ?string $virtual_tour_url = null;
190 public ?string $promo_video_url = null;
191 public ?int $social_share_image_id = null;
192
193 // SEO
194 public ?string $meta_title = null;
195 public ?string $meta_description = null;
196 public ?string $permalink = null;
197 public ?string $meta_keywords = null;
198 public ?string $og_title = null;
199 public ?string $og_description = null;
200 public ?int $og_image_id = null;
201 public ?string $schema_markup = null;
202
203 // Status & Lifecycle
204 public string $status = 'draft';
205 public ?string $scheduled_publish_date = null;
206 public ?string $scheduled_unpublish_date = null;
207 public ?string $published_at = null;
208 public int $version = 1;
209 public bool $is_featured = false;
210 public int $featured_order = 0;
211 public int $sort_order = 0;
212
213 // Analytics
214 public int $views_count = 0;
215 public int $bookings_count = 0;
216 public float $revenue_total = 0.00;
217 public float $conversion_rate = 0.00;
218 public float $avg_rating = 0.00;
219 public ?float $average_rating = null;
220 public int $reviews_count = 0;
221 /** Approved reviews count (frontend); must be declared so fromStdClass() copies it from SingleTripController. */
222 public int $review_count = 0;
223 public ?string $last_viewed_at = null;
224 public ?string $last_booked_at = null;
225
226 // JSON Fields (stored as arrays/objects)
227 public array $attributes = [];
228 public array $highlights = [];
229 public array $testimonials = [];
230 public array $countries = [];
231 public array $regions = [];
232 public array $landmarks = [];
233 public array $tags = [];
234 public array $included_items = [];
235 public array $excluded_items = [];
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 = [];
246 public array $price_types = [];
247 public array $itinerary_days = [];
248 public array $faqs = [];
249 public array $frontend_tabs = [];
250 public array $availability_dates = [];
251 public array $custom_fields = [];
252 public array $pricing_rules = [];
253 public array $booking_rules = [];
254
255 // Relationships & Linked Data
256 public array $destinations = [];
257 public array $activities = [];
258 public array $categories = [];
259 public array $included_services = [];
260 public array $excluded_services = [];
261 public array $equipment_list = [];
262 public array $packing_list = [];
263 public array $itinerary = [];
264 public array $reviews = [];
265 /** Per-star counts for approved reviews (from SQL); used when review list is capped. */
266 public array $rating_distribution = [];
267 public array $departures = [];
268 public array $bookings = [];
269 public array $downloadable_items = [];
270 public array $similar_trips = [];
271
272
273 // Timestamps
274 public string $created_at = '';
275 public string $updated_at = '';
276 public int $created_by = 0;
277 public int $updated_by = 0;
278 public ?string $deleted_at = null;
279 public ?int $deleted_by = null;
280
281 /**
282 * Create from array (database row)
283 */
284 public static function fromArray(array $data): self
285 {
286 $trip = new self();
287
288
289 // Basic fields
290 $trip->id = (int) ($data['id'] ?? 0);
291 $trip->title = $data['title'] ?? '';
292 $trip->slug = $data['slug'] ?? '';
293 $trip->trip_code = $data['trip_code'] ?? null;
294 $trip->description = $data['description'] ?? '';
295 $trip->short_description = $data['short_description'] ?? null;
296 $trip->trip_details = $data['trip_details'] ?? '';
297 $trip->what_makes_special = $data['what_makes_special'] ?? '';
298 $trip->trip_story = $data['trip_story'] ?? '';
299 $trip->excerpt = $data['excerpt'] ?? null;
300
301 // Location
302 $trip->starting_location = $data['starting_location'] ?? null;
303 $trip->ending_location = $data['ending_location'] ?? null;
304 $trip->latitude = $data['latitude'] ?? null;
305 $trip->longitude = $data['longitude'] ?? null;
306 $trip->map_zoom_level = (int) ($data['map_zoom_level'] ?? 10);
307 $trip->timezone = $data['timezone'] ?? null;
308 $trip->country_code = $data['country_code'] ?? null;
309
310 // Duration
311 $trip->trip_type = $data['trip_type'] ?? 'multi_day';
312 $trip->duration_days = isset($data['duration_days']) ? (int) $data['duration_days'] : null;
313 $trip->duration_nights = isset($data['duration_nights']) ? (int) $data['duration_nights'] : null;
314 $trip->duration_hours = isset($data['duration_hours']) ? (int) $data['duration_hours'] : null;
315 $trip->available_from = $data['available_from'] ?? null;
316 $trip->available_to = $data['available_to'] ?? null;
317 $trip->booking_window_days = (int) ($data['booking_window_days'] ?? 30);
318 $trip->booking_deadline_hours = (int) ($data['booking_deadline_hours'] ?? 24);
319 $trip->flexible_dates = (bool) ($data['flexible_dates'] ?? false);
320 $trip->fixed_departures_only = (bool) ($data['fixed_departures_only'] ?? false);
321
322 // Seasonal
323 $trip->seasonal_availability = $data['seasonal_availability'] ?? null;
324 $trip->best_season = $data['best_season'] ?? null;
325 $trip->peak_season = $data['peak_season'] ?? null;
326 $trip->off_season = $data['off_season'] ?? null;
327 $trip->seasonal_auto_enable = (bool) ($data['seasonal_auto_enable'] ?? false);
328 $trip->seasonal_enable_date = $data['seasonal_enable_date'] ?? null;
329 $trip->seasonal_disable_date = $data['seasonal_disable_date'] ?? null;
330 $trip->blackout_dates = self::parseJsonField($data['blackout_dates'] ?? null);
331
332 // Categorization
333 $trip->trip_category = $data['trip_category'] ?? null;
334 $trip->trip_category_parent = $data['trip_category_parent'] ?? null;
335 $trip->trip_category_sub = $data['trip_category_sub'] ?? null;
336 $trip->difficulty_level = $data['difficulty_level'] ?? null;
337 $trip->activity_intensity = $data['activity_intensity'] ?? null;
338 $trip->featured_priority = $data['featured_priority'] ?? 'none';
339 $trip->trip_style = $data['trip_style'] ?? null;
340 $trip->group_type = $data['group_type'] ?? 'both';
341
342 // Pricing
343 $trip->pricing_type = $data['pricing_type'] ?? 'regular';
344 $trip->original_price = (float) ($data['original_price'] ?? 0.00);
345 $trip->discounted_price = isset($data['discounted_price']) ? (float) $data['discounted_price'] : null;
346 $trip->sale_price = isset($data['sale_price']) ? (float) $data['sale_price'] : null;
347 $trip->currency = $data['currency'] ?? 'USD';
348 $trip->price_per_person = (bool) ($data['price_per_person'] ?? true);
349 $trip->deposit_required = (bool) ($data['deposit_required'] ?? false);
350 $trip->deposit_amount = isset($data['deposit_amount']) ? (float) $data['deposit_amount'] : null;
351 $trip->deposit_percentage = isset($data['deposit_percentage']) ? (float) $data['deposit_percentage'] : null;
352 $trip->payment_terms = $data['payment_terms'] ?? '';
353 $trip->payment_plans_enabled = (bool) ($data['payment_plans_enabled'] ?? false);
354 $trip->tax_included = (bool) ($data['tax_included'] ?? false);
355 $trip->tax_rate = isset($data['tax_rate']) ? (float) $data['tax_rate'] : null;
356 $trip->service_charge = isset($data['service_charge']) ? (float) $data['service_charge'] : null;
357 $trip->service_charge_percentage = isset($data['service_charge_percentage']) ? (float) $data['service_charge_percentage'] : null;
358
359 // Group Pricing
360 $trip->group_pricing_enabled = (bool) ($data['group_pricing_enabled'] ?? false);
361 $trip->group_size_min = isset($data['group_size_min']) ? (int) $data['group_size_min'] : null;
362 $trip->group_size_max = isset($data['group_size_max']) ? (int) $data['group_size_max'] : null;
363 $trip->group_discount_type = $data['group_discount_type'] ?? 'percentage';
364 $trip->group_discount_percentage = isset($data['group_discount_percentage']) ? (float) $data['group_discount_percentage'] : null;
365 $trip->group_discount_amount = isset($data['group_discount_amount']) ? (float) $data['group_discount_amount'] : null;
366
367 // Early Bird / Last Minute
368 $trip->early_bird_discount_enabled = (bool) ($data['early_bird_discount_enabled'] ?? false);
369 $trip->early_bird_days = isset($data['early_bird_days']) ? (int) $data['early_bird_days'] : null;
370 $trip->early_bird_discount = isset($data['early_bird_discount']) ? (float) $data['early_bird_discount'] : null;
371 $trip->last_minute_discount_enabled = (bool) ($data['last_minute_discount_enabled'] ?? false);
372 $trip->last_minute_days = isset($data['last_minute_days']) ? (int) $data['last_minute_days'] : null;
373 $trip->last_minute_discount = isset($data['last_minute_discount']) ? (float) $data['last_minute_discount'] : null;
374
375 // Booking
376 $trip->min_travelers = (int) ($data['min_travelers'] ?? 1);
377 $trip->max_travelers = isset($data['max_travelers']) ? (int) $data['max_travelers'] : null;
378 $trip->max_travelers_per_booking = isset($data['max_travelers_per_booking']) ? (int) $data['max_travelers_per_booking'] : null;
379 $trip->waitlist_enabled = (bool) ($data['waitlist_enabled'] ?? false);
380 $trip->waitlist_capacity = isset($data['waitlist_capacity']) ? (int) $data['waitlist_capacity'] : null;
381 $trip->instant_booking = (bool) ($data['instant_booking'] ?? true);
382 $trip->requires_approval = (bool) ($data['requires_approval'] ?? false);
383 $trip->booking_confirmation_email = (bool) ($data['booking_confirmation_email'] ?? true);
384 $trip->booking_reminder_email = (bool) ($data['booking_reminder_email'] ?? true);
385 $trip->reminder_days_before = (int) ($data['reminder_days_before'] ?? 7);
386
387 // Requirements
388 $trip->age_min = isset($data['age_min']) ? (int) $data['age_min'] : null;
389 $trip->age_max = isset($data['age_max']) ? (int) $data['age_max'] : null;
390 $trip->physical_requirements = $data['physical_requirements'] ?? '';
391 $trip->medical_requirements = $data['medical_requirements'] ?? '';
392 $trip->visa_requirements = $data['visa_requirements'] ?? '';
393 $trip->vaccination_requirements = $data['vaccination_requirements'] ?? '';
394 $trip->passport_validity_months = (int) ($data['passport_validity_months'] ?? 6);
395 $trip->travel_insurance_required = (bool) ($data['travel_insurance_required'] ?? false);
396 $trip->special_equipment = $data['special_equipment'] ?? '';
397
398 // Policies
399 $trip->cancellation_policy = $data['cancellation_policy'] ?? '';
400 $trip->refund_policy = $data['refund_policy'] ?? '';
401 $trip->change_policy = $data['change_policy'] ?? '';
402 $trip->weather_policy = $data['weather_policy'] ?? '';
403 $trip->force_majeure_policy = $data['force_majeure_policy'] ?? '';
404 $trip->terms_conditions = $data['terms_conditions'] ?? '';
405
406 // Accommodation
407 $trip->accommodation_type = $data['accommodation_type'] ?? null;
408 $trip->accommodation_standard = $data['accommodation_standard'] ?? null;
409 $trip->meal_plan = $data['meal_plan'] ?? null;
410 $trip->accommodation_details = $data['accommodation_details'] ?? '';
411 $trip->accommodation_included = (bool) ($data['accommodation_included'] ?? true);
412
413 // Transportation
414 $trip->transportation_included = (bool) ($data['transportation_included'] ?? false);
415 $trip->pickup_location = $data['pickup_location'] ?? null;
416 $trip->pickup_location_lat = $data['pickup_location_lat'] ?? null;
417 $trip->pickup_location_lng = $data['pickup_location_lng'] ?? null;
418 $trip->dropoff_location = $data['dropoff_location'] ?? null;
419 $trip->dropoff_location_lat = $data['dropoff_location_lat'] ?? null;
420 $trip->dropoff_location_lng = $data['dropoff_location_lng'] ?? null;
421 $trip->transportation_details = $data['transportation_details'] ?? '';
422 $trip->internal_transportation = $data['internal_transportation'] ?? '';
423 $trip->international_flights_included = (bool) ($data['international_flights_included'] ?? false);
424 $trip->domestic_flights_included = (bool) ($data['domestic_flights_included'] ?? false);
425
426 // Media
427 $trip->featured_image = isset($data['featured_image']) ? (int) $data['featured_image'] : null;
428 $trip->featured_image_url = $data['featured_image_url'] ?? null;
429 $trip->video_url = $data['video_url'] ?? null;
430 $trip->virtual_tour_url = $data['virtual_tour_url'] ?? null;
431 $trip->promo_video_url = $data['promo_video_url'] ?? null;
432 $trip->social_share_image_id = isset($data['social_share_image_id']) ? (int) $data['social_share_image_id'] : null;
433
434 // SEO
435 $trip->meta_title = $data['meta_title'] ?? null;
436 $trip->meta_description = $data['meta_description'] ?? null;
437 $trip->meta_keywords = $data['meta_keywords'] ?? null;
438 $trip->og_title = $data['og_title'] ?? null;
439 $trip->og_description = $data['og_description'] ?? null;
440 $trip->og_image_id = isset($data['og_image_id']) ? (int) $data['og_image_id'] : null;
441 $trip->schema_markup = $data['schema_markup'] ?? null;
442
443 // Status
444 $trip->status = $data['status'] ?? 'draft';
445 $trip->scheduled_publish_date = $data['scheduled_publish_date'] ?? null;
446 $trip->scheduled_unpublish_date = $data['scheduled_unpublish_date'] ?? null;
447 $trip->published_at = $data['published_at'] ?? null;
448 $trip->version = (int) ($data['version'] ?? 1);
449 $trip->is_featured = (bool) ($data['is_featured'] ?? false);
450 $trip->featured_order = (int) ($data['featured_order'] ?? 0);
451 $trip->sort_order = (int) ($data['sort_order'] ?? 0);
452
453 // Analytics
454 $trip->views_count = (int) ($data['views_count'] ?? 0);
455 $trip->bookings_count = (int) ($data['bookings_count'] ?? 0);
456 $trip->revenue_total = (float) ($data['revenue_total'] ?? 0.00);
457 $trip->conversion_rate = (float) ($data['conversion_rate'] ?? 0.00);
458 $trip->avg_rating = (float) ($data['avg_rating'] ?? 0.00);
459 $trip->reviews_count = (int) ($data['reviews_count'] ?? 0);
460 $trip->last_viewed_at = $data['last_viewed_at'] ?? null;
461 $trip->last_booked_at = $data['last_booked_at'] ?? null;
462
463 // JSON Fields
464 $trip->highlights = self::parseJsonField($data['highlights'] ?? null);
465 $trip->testimonials = self::parseJsonField($data['testimonials'] ?? null);
466 $trip->countries = self::parseJsonField($data['countries'] ?? null);
467 $trip->regions = self::parseJsonField($data['regions'] ?? null);
468 $trip->landmarks = self::parseJsonField($data['landmarks'] ?? null);
469 $trip->tags = self::parseJsonField($data['tags'] ?? null);
470 $trip->included_items = self::parseJsonField($data['included_items'] ?? null);
471 $trip->excluded_items = self::parseJsonField($data['excluded_items'] ?? null);
472 $trip->gallery_images = self::parseJsonField($data['gallery_images'] ?? null);
473 $trip->price_types = self::parseJsonField($data['price_types'] ?? null);
474 $trip->itinerary_days = self::parseJsonField($data['itinerary_days'] ?? null);
475 $trip->faqs = self::parseJsonField($data['faqs'] ?? null);
476 $trip->downloadable_items = self::parseJsonField($data['downloadable_items'] ?? null);
477 $trip->frontend_tabs = self::parseJsonField($data['frontend_tabs'] ?? null);
478 $trip->availability_dates = self::parseJsonField($data['availability_dates'] ?? null);
479 $trip->custom_fields = self::parseJsonField($data['custom_fields'] ?? null, []);
480 $trip->pricing_rules = self::parseJsonField($data['pricing_rules'] ?? null, []);
481 $trip->booking_rules = self::parseJsonField($data['booking_rules'] ?? null, []);
482
483 // Timestamps
484 $trip->created_at = $data['created_at'] ?? '';
485 $trip->updated_at = $data['updated_at'] ?? '';
486 $trip->created_by = (int) ($data['created_by'] ?? 0);
487 $trip->updated_by = (int) ($data['updated_by'] ?? 0);
488 $trip->deleted_at = $data['deleted_at'] ?? null;
489 $trip->deleted_by = isset($data['deleted_by']) ? (int) $data['deleted_by'] : null;
490
491 return $trip;
492 }
493
494 /**
495 * Convert to array (for database storage)
496 */
497 public function toArray(): array
498 {
499 return [
500 'id' => $this->id,
501 'title' => $this->title,
502 'slug' => $this->slug,
503 'trip_code' => $this->trip_code,
504 'description' => $this->description,
505 'short_description' => $this->short_description,
506 'trip_details' => $this->trip_details,
507 'what_makes_special' => $this->what_makes_special,
508 'trip_story' => $this->trip_story,
509 'excerpt' => $this->excerpt,
510 'starting_location' => $this->starting_location,
511 'ending_location' => $this->ending_location,
512 'latitude' => $this->latitude,
513 'longitude' => $this->longitude,
514 'map_zoom_level' => $this->map_zoom_level,
515 'timezone' => $this->timezone,
516 'country_code' => $this->country_code,
517 'trip_type' => $this->trip_type,
518 'duration_days' => $this->duration_days,
519 'duration_nights' => $this->duration_nights,
520 'duration_hours' => $this->duration_hours,
521 'available_from' => $this->available_from,
522 'available_to' => $this->available_to,
523 'booking_window_days' => $this->booking_window_days,
524 'booking_deadline_hours' => $this->booking_deadline_hours,
525 'flexible_dates' => $this->flexible_dates ? 1 : 0,
526 'fixed_departures_only' => $this->fixed_departures_only ? 1 : 0,
527 'seasonal_availability' => $this->seasonal_availability,
528 'best_season' => $this->best_season,
529 'peak_season' => $this->peak_season,
530 'off_season' => $this->off_season,
531 'seasonal_auto_enable' => $this->seasonal_auto_enable ? 1 : 0,
532 'seasonal_enable_date' => $this->seasonal_enable_date,
533 'seasonal_disable_date' => $this->seasonal_disable_date,
534 'blackout_dates' => self::serializeJsonField($this->blackout_dates),
535 'trip_category' => $this->trip_category,
536 'trip_category_parent' => $this->trip_category_parent,
537 'trip_category_sub' => $this->trip_category_sub,
538 'difficulty_level' => $this->difficulty_level,
539 'activity_intensity' => $this->activity_intensity,
540 'featured_priority' => $this->featured_priority,
541 'trip_style' => $this->trip_style,
542 'group_type' => $this->group_type,
543 'pricing_type' => $this->pricing_type,
544 'original_price' => $this->original_price,
545 'discounted_price' => $this->discounted_price,
546 'sale_price' => $this->sale_price,
547 'currency' => $this->currency,
548 'price_per_person' => $this->price_per_person ? 1 : 0,
549 'deposit_required' => $this->deposit_required ? 1 : 0,
550 'deposit_amount' => $this->deposit_amount,
551 'deposit_percentage' => $this->deposit_percentage,
552 'payment_terms' => $this->payment_terms,
553 'payment_plans_enabled' => $this->payment_plans_enabled ? 1 : 0,
554 'tax_included' => $this->tax_included ? 1 : 0,
555 'tax_rate' => $this->tax_rate,
556 'service_charge' => $this->service_charge,
557 'service_charge_percentage' => $this->service_charge_percentage,
558 'group_pricing_enabled' => $this->group_pricing_enabled ? 1 : 0,
559 'group_size_min' => $this->group_size_min,
560 'group_size_max' => $this->group_size_max,
561 'group_discount_type' => $this->group_discount_type,
562 'group_discount_percentage' => $this->group_discount_percentage,
563 'group_discount_amount' => $this->group_discount_amount,
564 'early_bird_discount_enabled' => $this->early_bird_discount_enabled ? 1 : 0,
565 'early_bird_days' => $this->early_bird_days,
566 'early_bird_discount' => $this->early_bird_discount,
567 'last_minute_discount_enabled' => $this->last_minute_discount_enabled ? 1 : 0,
568 'last_minute_days' => $this->last_minute_days,
569 'last_minute_discount' => $this->last_minute_discount,
570 'min_travelers' => $this->min_travelers,
571 'max_travelers' => $this->max_travelers,
572 'max_travelers_per_booking' => $this->max_travelers_per_booking,
573 'waitlist_enabled' => $this->waitlist_enabled ? 1 : 0,
574 'waitlist_capacity' => $this->waitlist_capacity,
575 'instant_booking' => $this->instant_booking ? 1 : 0,
576 'requires_approval' => $this->requires_approval ? 1 : 0,
577 'booking_confirmation_email' => $this->booking_confirmation_email ? 1 : 0,
578 'booking_reminder_email' => $this->booking_reminder_email ? 1 : 0,
579 'reminder_days_before' => $this->reminder_days_before,
580 'age_min' => $this->age_min,
581 'age_max' => $this->age_max,
582 'physical_requirements' => $this->physical_requirements,
583 'medical_requirements' => $this->medical_requirements,
584 'visa_requirements' => $this->visa_requirements,
585 'vaccination_requirements' => $this->vaccination_requirements,
586 'passport_validity_months' => $this->passport_validity_months,
587 'travel_insurance_required' => $this->travel_insurance_required ? 1 : 0,
588 'special_equipment' => $this->special_equipment,
589 'cancellation_policy' => $this->cancellation_policy,
590 'refund_policy' => $this->refund_policy,
591 'change_policy' => $this->change_policy,
592 'weather_policy' => $this->weather_policy,
593 'force_majeure_policy' => $this->force_majeure_policy,
594 'terms_conditions' => $this->terms_conditions,
595 'accommodation_type' => $this->accommodation_type,
596 'accommodation_standard' => $this->accommodation_standard,
597 'meal_plan' => $this->meal_plan,
598 'accommodation_details' => $this->accommodation_details,
599 'accommodation_included' => $this->accommodation_included ? 1 : 0,
600 'transportation_included' => $this->transportation_included ? 1 : 0,
601 'pickup_location' => $this->pickup_location,
602 'pickup_location_lat' => $this->pickup_location_lat,
603 'pickup_location_lng' => $this->pickup_location_lng,
604 'dropoff_location' => $this->dropoff_location,
605 'dropoff_location_lat' => $this->dropoff_location_lat,
606 'dropoff_location_lng' => $this->dropoff_location_lng,
607 'transportation_details' => $this->transportation_details,
608 'internal_transportation' => $this->internal_transportation,
609 'international_flights_included' => $this->international_flights_included ? 1 : 0,
610 'domestic_flights_included' => $this->domestic_flights_included ? 1 : 0,
611 'featured_image' => $this->featured_image,
612 'featured_image_url' => $this->featured_image_url,
613 'video_url' => $this->video_url,
614 'virtual_tour_url' => $this->virtual_tour_url,
615 'promo_video_url' => $this->promo_video_url,
616 'social_share_image_id' => $this->social_share_image_id,
617 'meta_title' => $this->meta_title,
618 'meta_description' => $this->meta_description,
619 'meta_keywords' => $this->meta_keywords,
620 'og_title' => $this->og_title,
621 'og_description' => $this->og_description,
622 'og_image_id' => $this->og_image_id,
623 'schema_markup' => $this->schema_markup,
624 'status' => $this->status,
625 'scheduled_publish_date' => $this->scheduled_publish_date,
626 'scheduled_unpublish_date' => $this->scheduled_unpublish_date,
627 'published_at' => $this->published_at,
628 'version' => $this->version,
629 'is_featured' => $this->is_featured ? 1 : 0,
630 'featured_order' => $this->featured_order,
631 'sort_order' => $this->sort_order,
632 'views_count' => $this->views_count,
633 'bookings_count' => $this->bookings_count,
634 'revenue_total' => $this->revenue_total,
635 'conversion_rate' => $this->conversion_rate,
636 'avg_rating' => $this->avg_rating,
637 'reviews_count' => $this->reviews_count,
638 'last_viewed_at' => $this->last_viewed_at,
639 'last_booked_at' => $this->last_booked_at,
640 'highlights' => self::serializeJsonField($this->highlights),
641 'testimonials' => self::serializeJsonField($this->testimonials),
642 'countries' => self::serializeJsonField($this->countries),
643 'regions' => self::serializeJsonField($this->regions),
644 'landmarks' => self::serializeJsonField($this->landmarks),
645 'tags' => self::serializeJsonField($this->tags),
646 'included_items' => self::serializeJsonField($this->included_items),
647 'excluded_items' => self::serializeJsonField($this->excluded_items),
648 'gallery_images' => self::serializeJsonField($this->gallery_images),
649 'price_types' => self::serializeJsonField($this->price_types),
650 'itinerary_days' => self::serializeJsonField($this->itinerary_days),
651 'faqs' => self::serializeJsonField($this->faqs),
652 'frontend_tabs' => self::serializeJsonField($this->frontend_tabs),
653 'availability_dates' => self::serializeJsonField($this->availability_dates),
654 'custom_fields' => self::serializeJsonField($this->custom_fields),
655 'pricing_rules' => self::serializeJsonField($this->pricing_rules),
656 'booking_rules' => self::serializeJsonField($this->booking_rules),
657 'created_at' => $this->created_at,
658 'updated_at' => $this->updated_at,
659 'created_by' => $this->created_by,
660 'updated_by' => $this->updated_by,
661 'deleted_at' => $this->deleted_at,
662 'deleted_by' => $this->deleted_by,
663 ];
664 }
665
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 /**
680 * Parse JSON field from database
681 */
682 private static function parseJsonField(?string $value, array $default = []): array
683 {
684 if (empty($value)) {
685 return $default;
686 }
687
688 $decoded = maybe_unserialize($value);
689 if (is_array($decoded)) {
690 return $decoded;
691 }
692
693 $json = json_decode($value, true);
694 if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
695 return $json;
696 }
697
698 return $default;
699 }
700
701 /**
702 * Serialize JSON field for database
703 */
704 private static function serializeJsonField(array $value): ?string
705 {
706 if (empty($value)) {
707 return null;
708 }
709
710 return maybe_serialize($value);
711 }
712
713 // ========================================
714 // GETTER METHODS FOR TRIP CARD COMPONENT
715 // ========================================
716
717 /**
718 * Get formatted trip title
719 */
720 public function getTitle(): string
721 {
722 return !empty($this->title) ? $this->title : __('Untitled Trip', 'yatra');
723 }
724
725 /**
726 * Get short description
727 */
728 public function getShortDescription(): ?string
729 {
730 return !empty($this->short_description) ? $this->short_description : null;
731 }
732
733 /**
734 * Get discount information
735 */
736 public function getDiscount(): array
737 {
738 // Check pricing type for traveler-based pricing
739 $is_traveler_based = (!empty($this->pricing_type) && $this->pricing_type === 'traveler_based');
740
741 $has_discount = false;
742 $discount_percent = 0;
743 $discount_text = '';
744
745 if ($is_traveler_based) {
746 // For traveler-based pricing, calculate highest discount from all categories
747 $max_discount = 0;
748
749 // Check multiple possible data structures for price categories
750 $price_categories = [];
751
752 // Try different property names for price categories
753 if (!empty($this->price_types) && is_array($this->price_types)) {
754 $price_categories = $this->price_types;
755 } elseif (!empty($this->pricing_categories) && is_array($this->pricing_categories)) {
756 $price_categories = $this->pricing_categories;
757 } elseif (!empty($this->traveler_categories) && is_array($this->traveler_categories)) {
758 $price_categories = $this->traveler_categories;
759 }
760
761 if (!empty($price_categories)) {
762 foreach ($price_categories as $category) {
763 // Handle both object and array formats
764 if (is_object($category)) {
765 $regular_price = (float) ($category->original_price ?? $category->price ?? 0);
766 $discounted_price = (float) ($category->discounted_price ?? $category->sale_price ?? $category->discount_price ?? 0);
767 } elseif (is_array($category)) {
768 $regular_price = (float) ($category['original_price'] ?? $category['price'] ?? 0);
769 $discounted_price = (float) ($category['discounted_price'] ?? $category['sale_price'] ?? $category['discount_price'] ?? 0);
770 } else {
771 continue;
772 }
773
774 if ($regular_price > 0 && $discounted_price > 0 && $discounted_price < $regular_price) {
775 $category_discount = round((($regular_price - $discounted_price) / $regular_price) * 100);
776 if ($category_discount > $max_discount) {
777 $max_discount = $category_discount;
778 }
779 }
780 }
781 }
782
783 // Fallback to pre-calculated discount data
784 if ($max_discount === 0) {
785 if (!empty($this->max_discount_percentage)) {
786 $max_discount = (int) $this->max_discount_percentage;
787 } elseif (!empty($this->discount_percentage)) {
788 $max_discount = (int) $this->discount_percentage;
789 } elseif (!empty($this->highest_discount)) {
790 $max_discount = (int) $this->highest_discount;
791 }
792 }
793
794 if ($max_discount > 0) {
795 $discount_percent = $max_discount;
796 $discount_text = 'Up to ' . $discount_percent . '%';
797 $has_discount = true;
798 }
799 } else {
800 // Regular pricing - calculate discount from original vs sale price
801 $original_price = 0;
802 $sale_price = 0;
803
804 // Check for original price
805 if (!empty($this->original_price)) {
806 $original_price = (float) $this->original_price;
807 } elseif (!empty($this->base_price)) {
808 $original_price = (float) $this->base_price;
809 }
810
811 // Check for sale/discounted price
812 if (!empty($this->sale_price)) {
813 $sale_price = (float) $this->sale_price;
814 } elseif (!empty($this->discounted_price)) {
815 $sale_price = (float) $this->discounted_price;
816 }
817
818 if ($sale_price > 0 && $sale_price < $original_price && $original_price > 0) {
819 $discount_percent = round((($original_price - $sale_price) / $original_price) * 100);
820 // Only show discount if percentage is greater than 0
821 if ($discount_percent > 0) {
822 $discount_text = $discount_percent . '%';
823 $has_discount = true;
824 }
825 }
826 }
827
828 return [
829 'has_discount' => $has_discount,
830 'discount_percent' => $discount_percent,
831 'discount_text' => $discount_text,
832 'is_traveler_based' => $is_traveler_based
833 ];
834 }
835
836 /**
837 * Get trip destinations
838 */
839 public function getDestinations(): array
840 {
841 // This will be populated by the repository when loading trip data
842 return $this->destinations ?? [];
843 }
844
845 /**
846 * Get trip categories
847 */
848 public function getCategories(): array
849 {
850 // Handle both property names for backward compatibility
851 // AppServiceProvider uses 'categories', TripRepository uses 'trip_category'
852 if (!empty($this->categories)) {
853 return $this->categories;
854 }
855
856 if (!empty($this->trip_category)) {
857 return $this->trip_category;
858 }
859
860 return [];
861 }
862
863 /**
864 * Get trip rating information
865 */
866 public function getRating(): array
867 {
868 $average_rating = (float) ($this->average_rating ?? 0);
869 $review_count = (int) ($this->review_count ?? 0);
870
871 return [
872 'average_rating' => $average_rating,
873 'review_count' => $review_count,
874 'formatted_rating' => number_format($average_rating, 1),
875 'has_rating' => $average_rating > 0 && $review_count > 0
876 ];
877 }
878
879 /**
880 * Get pricing information
881 */
882 public function getPricing(): array
883 {
884 // Check pricing type for traveler-based pricing
885 $is_traveler_based = (!empty($this->pricing_type) && $this->pricing_type === 'traveler_based');
886
887 $current_price = '';
888 $original_price = '';
889 $price_prefix = '';
890 $has_discount = false;
891 $current_price_raw = 0;
892 $original_price_raw = 0;
893
894 if ($is_traveler_based) {
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;
899 $min_price = PHP_FLOAT_MAX;
900 $min_original_price = 0.0;
901
902 // Check multiple possible data structures for price categories
903 $price_categories = [];
904
905 // Try different property names for price categories
906 if (!empty($this->price_types) && is_array($this->price_types)) {
907 $price_categories = $this->price_types;
908 } elseif (!empty($this->pricing_categories) && is_array($this->pricing_categories)) {
909 $price_categories = $this->pricing_categories;
910 } elseif (!empty($this->traveler_categories) && is_array($this->traveler_categories)) {
911 $price_categories = $this->traveler_categories;
912 }
913
914 if (!empty($price_categories)) {
915 foreach ($price_categories as $category) {
916 // Handle both object and array formats
917 if (is_object($category)) {
918 $regular_price = (float) ($category->original_price ?? $category->price ?? 0);
919 // Priority: discounted_price > sale_price (legacy) > discount_price (legacy)
920 $discounted_price = (float) ($category->discounted_price ?? $category->sale_price ?? $category->discount_price ?? 0);
921 $is_default = !empty($category->is_default);
922 } elseif (is_array($category)) {
923 $regular_price = (float) ($category['original_price'] ?? $category['price'] ?? 0);
924 // Priority: discounted_price > sale_price (legacy) > discount_price (legacy)
925 $discounted_price = (float) ($category['discounted_price'] ?? $category['sale_price'] ?? $category['discount_price'] ?? 0);
926 $is_default = !empty($category['is_default']);
927 } else {
928 continue;
929 }
930
931 // Use discounted price if available, otherwise regular price
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 }
941
942 if ($category_price > 0 && $category_price < $min_price) {
943 $min_price = $category_price;
944 // Store original price from the same category for strikethrough
945 if ($discounted_price > 0 && $discounted_price < $regular_price) {
946 $min_original_price = $regular_price;
947 $has_discount = true;
948 }
949 }
950 }
951 }
952
953 // Fallback to effective_price_min if no price_types
954 if ($min_price === PHP_FLOAT_MAX) {
955 if (!empty($this->effective_price_min)) {
956 $min_price = (float) $this->effective_price_min;
957 if (!empty($this->min_category_original_price)) {
958 $min_original_price = (float) $this->min_category_original_price;
959 $has_discount = $min_original_price > $min_price;
960 }
961 } elseif (!empty($this->min_price)) {
962 $min_price = (float) $this->min_price;
963 } elseif (!empty($this->starting_price)) {
964 $min_price = (float) $this->starting_price;
965 }
966 }
967
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;
976 $current_price = yatra_format_price($current_price_raw);
977 $price_prefix = __('From ', 'yatra'); // Always show "From" for traveler-based
978
979 if ($chosen_has_discount && $chosen_original > 0) {
980 $original_price_raw = $chosen_original;
981 $original_price = yatra_format_price($original_price_raw);
982 $has_discount = true;
983 } else {
984 $has_discount = false;
985 }
986 }
987 } else {
988 // Regular pricing - NO "From" text
989 if (!empty($this->original_price)) {
990 $original_price_raw = (float) $this->original_price;
991 } elseif (!empty($this->base_price)) {
992 $original_price_raw = (float) $this->base_price;
993 }
994
995 // Get discounted price (we only use discounted_price now, sale_price is deprecated)
996 $sale_price_raw = 0;
997 if (!empty($this->discounted_price)) {
998 $sale_price_raw = (float) $this->discounted_price;
999 } elseif (!empty($this->sale_price)) {
1000 // Fallback for legacy data that might still have sale_price
1001 $sale_price_raw = (float) $this->sale_price;
1002 }
1003
1004 $has_discount = $sale_price_raw > 0 && $sale_price_raw < $original_price_raw;
1005 $current_price_raw = $has_discount ? $sale_price_raw : $original_price_raw;
1006
1007 if ($current_price_raw > 0) {
1008 $current_price = yatra_format_price($current_price_raw);
1009 $price_prefix = ''; // NO "From" text for regular pricing
1010
1011 if ($has_discount && $original_price_raw > 0) {
1012 $original_price = yatra_format_price($original_price_raw);
1013 }
1014 }
1015 }
1016
1017 return [
1018 'has_price' => $current_price_raw > 0,
1019 'current_price' => $current_price,
1020 'original_price' => $original_price,
1021 'price_prefix' => $price_prefix,
1022 'has_discount' => $has_discount,
1023 'raw_current_price' => $current_price_raw,
1024 'raw_original_price' => $original_price_raw,
1025 'is_traveler_based' => $is_traveler_based
1026 ];
1027 }
1028
1029 /**
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}
1036 */
1037 public static function resolveDifficultyDisplay(
1038 ?string $difficultyLevel,
1039 ?string $difficultyName = null,
1040 $difficultyIcon = null
1041 ): array {
1042 $difficulty = '';
1043 $difficulty_icon = '';
1044 /** @var array<string, mixed>|null $icon_picker */
1045 $icon_picker = null;
1046
1047 if (!empty($difficultyLevel) && is_numeric($difficultyLevel) && (int) $difficultyLevel > 0) {
1048 global $wpdb;
1049
1050 $difficulty_data = $wpdb->get_row($wpdb->prepare(
1051 'SELECT * FROM ' . ClassificationsTable::getTableName() . ' WHERE id = %d AND type = %s',
1052 (int) $difficultyLevel,
1053 ClassificationTypes::DIFFICULTY
1054 ));
1055
1056 if ($difficulty_data) {
1057 $difficulty = (string) $difficulty_data->name;
1058 if (!empty($difficulty_data->icon)) {
1059 $icon_data = maybe_unserialize($difficulty_data->icon);
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 }
1065 } elseif (is_string($difficulty_data->icon)) {
1066 $difficulty_icon = $difficulty_data->icon;
1067 $icon_picker = [
1068 'type' => 'icon',
1069 'value' => $difficulty_icon,
1070 'provider' => 'yatra',
1071 ];
1072 }
1073 }
1074 }
1075 }
1076
1077 if ($difficulty === '' && !empty($difficultyName)) {
1078 $difficulty = $difficultyName;
1079 }
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 ];
1100 }
1101 }
1102
1103 if ($difficulty !== '' && $icon_picker === null && $difficulty_icon === '') {
1104 $difficulty_icon = 'mountain';
1105 $icon_picker = [
1106 'type' => 'icon',
1107 'value' => 'mountain',
1108 'provider' => 'yatra',
1109 ];
1110 }
1111
1112 return [
1113 'level' => $difficulty,
1114 'icon' => $difficulty_icon,
1115 'icon_picker' => $icon_picker,
1116 'has_difficulty' => $difficulty !== '',
1117 ];
1118 }
1119
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 /**
1133 * Get trip duration information
1134 */
1135 public function getDuration(): array
1136 {
1137 $duration = '';
1138 if ($this->isHoursBased()) {
1139 $duration = yatra_format_duration(0, null, $this->getDurationHours());
1140 } elseif (!empty($this->duration_days)) {
1141 $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null);
1142 }
1143
1144 return [
1145 'formatted' => $duration,
1146 'days' => $this->duration_days ?? 0,
1147 'nights' => $this->duration_nights ?? 0,
1148 'hours' => $this->getDurationHours(),
1149 'has_duration' => !empty($duration)
1150 ];
1151 }
1152
1153 /**
1154 * Get trip activities
1155 */
1156 public function getActivities(): array
1157 {
1158 // This will be populated by the repository when loading trip data
1159 return $this->activities ?? [];
1160 }
1161
1162 /**
1163 * Get trip image information
1164 */
1165 public function getImage(): array
1166 {
1167 $image_url = '';
1168 $has_image = false;
1169
1170 // Handle featured_image attachment ID
1171 if (!empty($this->featured_image) && is_numeric($this->featured_image)) {
1172 $attachment_url = wp_get_attachment_image_url((int) $this->featured_image, 'large');
1173 if ($attachment_url) {
1174 $image_url = $attachment_url;
1175 $has_image = true;
1176 }
1177 }
1178
1179 // Fallback to featured_image_url if it exists (for backward compatibility)
1180 if (empty($image_url) && !empty($this->featured_image_url)) {
1181 $image_url = $this->featured_image_url;
1182 $has_image = true;
1183 }
1184
1185 // Use placeholder SVG when no image exists
1186 if (empty($image_url)) {
1187 $image_url = plugins_url('assets/images/trip-placeholder.svg', YATRA_PLUGIN_FILE);
1188 $has_image = false;
1189 }
1190
1191 return [
1192 'url' => $image_url,
1193 'alt' => $this->getTitle(),
1194 'has_image' => $has_image
1195 ];
1196 }
1197
1198 /**
1199 * Get gallery image URLs from attachment IDs
1200 */
1201 public function getGalleryImageUrls(): array
1202 {
1203 $urls = [];
1204
1205 if (empty($this->gallery_images) || !is_array($this->gallery_images)) {
1206 return $urls;
1207 }
1208
1209 foreach ($this->gallery_images as $attachment_id) {
1210 if (is_numeric($attachment_id)) {
1211 $url = wp_get_attachment_image_url((int) $attachment_id, 'large');
1212 if ($url) {
1213 $urls[] = $url;
1214 }
1215 }
1216 }
1217
1218 return $urls;
1219 }
1220
1221 /**
1222 * Get trip permalink
1223 */
1224 public function getPermalink(): string
1225 {
1226 // If permalink is already set, use it
1227 if (!empty($this->permalink)) {
1228 return $this->permalink;
1229 }
1230
1231 // Use the proper helper function that handles trip base settings
1232 if (!empty($this->id)) {
1233 $permalink = yatra_get_trip_permalink($this);
1234
1235 // Debug logging for development
1236 if (defined('WP_DEBUG') && WP_DEBUG) {
1237 }
1238
1239 return $permalink;
1240 }
1241
1242 return '';
1243 }
1244
1245 /**
1246 * Check if trip is available for booking
1247 */
1248 public function isAvailableForBooking(): bool
1249 {
1250 // Check if trip is published
1251 if ($this->status !== 'publish') {
1252 return false;
1253 }
1254
1255 // Check if within available date range
1256 if (!empty($this->available_from) && strtotime($this->available_from) > time()) {
1257 return false;
1258 }
1259
1260 if (!empty($this->available_to) && strtotime($this->available_to) < time()) {
1261 return false;
1262 }
1263
1264 // Check capacity if specified
1265 if (!empty($this->max_travelers) && $this->max_travelers <= 0) {
1266 return false;
1267 }
1268
1269 return true;
1270 }
1271
1272 /**
1273 * Calculate effective price considering discounts
1274 */
1275 public function getEffectivePrice(): float
1276 {
1277 if ($this->pricing_type === 'traveler_based') {
1278 // Use centralized logic (default category if set, else minimum).
1279 return (float) \Yatra\Services\TripPricingService::getEffectivePrice((object) $this);
1280 }
1281
1282 // Use discounted price if available and valid
1283 if (!empty($this->discounted_price) && $this->discounted_price < $this->original_price) {
1284 return $this->discounted_price;
1285 }
1286
1287 // Use sale price if available and valid
1288 if (!empty($this->sale_price) && $this->sale_price < $this->original_price) {
1289 return $this->sale_price;
1290 }
1291
1292 return $this->original_price;
1293 }
1294
1295 /**
1296 * Check if trip has active discount
1297 */
1298 public function hasDiscount(): bool
1299 {
1300 if ($this->pricing_type === 'traveler_based') {
1301 return false; // Traveler-based pricing doesn't have simple discounts
1302 }
1303
1304 $effectivePrice = $this->getEffectivePrice();
1305 return $effectivePrice < $this->original_price;
1306 }
1307
1308 /**
1309 * Calculate discount percentage
1310 */
1311 public function getDiscountPercentage(): int
1312 {
1313 if (!$this->hasDiscount()) {
1314 return 0;
1315 }
1316
1317 $effectivePrice = $this->getEffectivePrice();
1318 $discount = (($this->original_price - $effectivePrice) / $this->original_price) * 100;
1319
1320 return (int) round($discount);
1321 }
1322
1323 /**
1324 * Check if deposit is required
1325 */
1326 public function requiresDeposit(): bool
1327 {
1328 return $this->deposit_required && ($this->deposit_amount > 0 || $this->deposit_percentage > 0);
1329 }
1330
1331 /**
1332 * Calculate deposit amount
1333 */
1334 public function getDepositAmount(): float
1335 {
1336 if (!$this->requiresDeposit()) {
1337 return 0.0;
1338 }
1339
1340 if ($this->deposit_amount > 0) {
1341 return $this->deposit_amount;
1342 }
1343
1344 if ($this->deposit_percentage > 0) {
1345 $effectivePrice = $this->getEffectivePrice();
1346 return ($effectivePrice * $this->deposit_percentage) / 100;
1347 }
1348
1349 return 0.0;
1350 }
1351
1352 /**
1353 * Check if trip is featured
1354 */
1355 public function isFeatured(): bool
1356 {
1357 return $this->featured_priority !== 'none' && !empty($this->featured_priority);
1358 }
1359
1360 /**
1361 * Get trip difficulty information
1362 */
1363 public function getDifficultyInfo(): array
1364 {
1365 return [
1366 'level' => $this->difficulty_level ?? 'moderate',
1367 'name' => $this->difficulty_name ?? ucfirst($this->difficulty_level ?? 'Moderate'),
1368 'icon' => $this->difficulty_icon ?? '',
1369 'has_difficulty' => !empty($this->difficulty_level)
1370 ];
1371 }
1372
1373 /**
1374 * Check if trip supports group bookings
1375 */
1376 public function supportsGroupBookings(): bool
1377 {
1378 return $this->group_pricing_enabled &&
1379 !empty($this->group_size_min) &&
1380 $this->group_size_min > 1;
1381 }
1382
1383 /**
1384 * Calculate group discount for given size
1385 */
1386 public function getGroupDiscount(int $groupSize): float
1387 {
1388 if (!$this->supportsGroupBookings() || $groupSize < $this->group_size_min) {
1389 return 0.0;
1390 }
1391
1392 if (!empty($this->group_size_max) && $groupSize > $this->group_size_max) {
1393 $groupSize = $this->group_size_max; // Cap at maximum
1394 }
1395
1396 if ($this->group_discount_type === 'percentage' && !empty($this->group_discount_percentage)) {
1397 return $this->group_discount_percentage;
1398 }
1399
1400 if ($this->group_discount_type === 'amount' && !empty($this->group_discount_amount)) {
1401 $effectivePrice = $this->getEffectivePrice();
1402 return ($this->group_discount_amount / $effectivePrice) * 100;
1403 }
1404
1405 return 0.0;
1406 }
1407
1408 /**
1409 * Validate trip data integrity
1410 */
1411 public function validate(): array
1412 {
1413 $errors = [];
1414
1415 // Required fields
1416 if (empty($this->title)) {
1417 $errors['title'] = 'Trip title is required';
1418 }
1419
1420 if (empty($this->slug)) {
1421 $errors['slug'] = 'Trip slug is required';
1422 }
1423
1424 // Pricing validation
1425 if ($this->original_price <= 0) {
1426 $errors['original_price'] = 'Original price must be greater than zero';
1427 }
1428
1429 if (!empty($this->discounted_price) && $this->discounted_price >= $this->original_price) {
1430 $errors['discounted_price'] = 'Discounted price must be less than original price';
1431 }
1432
1433 // Duration validation
1434 if ($this->trip_type === 'single_day' && $this->duration_days !== 1) {
1435 $errors['duration_days'] = 'Single day trips must have duration of 1 day';
1436 }
1437
1438 if ($this->trip_type === 'multi_day' && (!empty($this->duration_days) && $this->duration_days < 2)) {
1439 $errors['duration_days'] = 'Multi-day trips must have duration of at least 2 days';
1440 }
1441
1442 // Capacity validation
1443 if (!empty($this->max_travelers) && $this->max_travelers <= 0) {
1444 $errors['max_travelers'] = 'Maximum travelers must be greater than zero';
1445 }
1446
1447 // Group pricing validation
1448 if ($this->group_pricing_enabled) {
1449 if (empty($this->group_size_min) || $this->group_size_min <= 1) {
1450 $errors['group_size_min'] = 'Group minimum size must be greater than 1';
1451 }
1452
1453 if (!empty($this->group_size_max) && $this->group_size_max < $this->group_size_min) {
1454 $errors['group_size_max'] = 'Group maximum size must be greater than minimum size';
1455 }
1456 }
1457
1458 return $errors;
1459 }
1460
1461 /**
1462 * Get trip status information
1463 */
1464 public function getStatusInfo(): array
1465 {
1466 $statusLabels = [
1467 'draft' => 'Draft',
1468 'publish' => 'Published',
1469 'private' => 'Private',
1470 'trash' => 'Trashed'
1471 ];
1472
1473 return [
1474 'status' => $this->status,
1475 'label' => $statusLabels[$this->status] ?? 'Unknown',
1476 'is_published' => $this->status === 'publish',
1477 'is_draft' => $this->status === 'draft'
1478 ];
1479 }
1480
1481 /**
1482 * Get description
1483 */
1484 public function getDescription(): string
1485 {
1486 return $this->description ?? '';
1487 }
1488
1489 /**
1490 * Get highlights
1491 */
1492 public function getHighlights(): array
1493 {
1494 return is_array($this->highlights) ? $this->highlights : [];
1495 }
1496
1497 /**
1498 * Get FAQs
1499 */
1500 public function getFaqs(): array
1501 {
1502 return is_array($this->faqs) ? $this->faqs : [];
1503 }
1504
1505 /**
1506 * Get included items
1507 */
1508 public function getIncludedItems(): array
1509 {
1510 return is_array($this->included_items) ? $this->included_items : [];
1511 }
1512
1513 /**
1514 * Get excluded items
1515 */
1516 public function getExcludedItems(): array
1517 {
1518 return is_array($this->excluded_items) ? $this->excluded_items : [];
1519 }
1520
1521 /**
1522 * Get downloadable items
1523 */
1524 public function getDownloadableItems(): array
1525 {
1526 return is_array($this->downloadable_items) ? $this->downloadable_items : [];
1527 }
1528
1529 /**
1530 * Get landmarks
1531 */
1532 public function getLandmarks(): array
1533 {
1534 return is_array($this->landmarks) ? $this->landmarks : [];
1535 }
1536
1537 /**
1538 * Get attributes
1539 */
1540 public function getAttributes(): array
1541 {
1542 return is_array($this->attributes) ? $this->attributes : [];
1543 }
1544
1545 /**
1546 * Get trip type
1547 */
1548 public function getTripType(): string
1549 {
1550 return $this->trip_type ?? 'multiple_days';
1551 }
1552
1553 /**
1554 * Get duration days
1555 */
1556 public function getDurationDays(): int
1557 {
1558 return $this->duration_days ?? 0;
1559 }
1560
1561 /**
1562 * Get duration nights
1563 */
1564 public function getDurationNights(): int
1565 {
1566 return $this->duration_nights ?? 0;
1567 }
1568
1569 /**
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.
1594 */
1595 public function getDifficultyLevel(): string
1596 {
1597 return $this->difficulty_level ?? '';
1598 }
1599
1600 /**
1601 * Get min travelers
1602 */
1603 public function getMinTravelers(): int
1604 {
1605 return $this->min_travelers ?? 1;
1606 }
1607
1608 /**
1609 * Get max travelers
1610 */
1611 public function getMaxTravelers(): int
1612 {
1613 return $this->max_travelers ?? 20;
1614 }
1615
1616 /**
1617 * Get original price
1618 */
1619 public function getOriginalPrice(): float
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 }
1625 return (float) ($this->original_price ?? 0);
1626 }
1627
1628 /**
1629 * Get sale price
1630 */
1631 public function getSalePrice(): float
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 }
1637 return (float) ($this->sale_price ?? 0);
1638 }
1639
1640 /**
1641 * Get price (alias for getEffectivePrice for backward compatibility)
1642 */
1643 public function getPrice(): float
1644 {
1645 return $this->getEffectivePrice();
1646 }
1647
1648 /**
1649 * Get price types
1650 */
1651 public function getPriceTypes(): array
1652 {
1653 return is_array($this->price_types) ? $this->price_types : [];
1654 }
1655
1656 /**
1657 * Get pricing type
1658 */
1659 public function getPricingType(): string
1660 {
1661 return $this->pricing_type ?? 'regular';
1662 }
1663
1664 /**
1665 * Get availability dates
1666 */
1667 public function getAvailabilityDates(): array
1668 {
1669 return is_array($this->availability_dates) ? $this->availability_dates : [];
1670 }
1671
1672 /**
1673 * Get booking mode
1674 */
1675 public function getBookingMode(): string
1676 {
1677 return $this->booking_mode ?? 'flexible';
1678 }
1679
1680 /**
1681 * Check if trip has specific availability configured
1682 */
1683 public function getHasSpecificAvailability(): bool
1684 {
1685 return $this->has_specific_availability ?? false;
1686 }
1687
1688 /**
1689 * Check if trip has booking capability
1690 */
1691 public function getHasBookingCapability(): bool
1692 {
1693 return $this->has_booking_capability ?? true;
1694 }
1695
1696 /**
1697 * Check if trip has default time slots enabled
1698 */
1699 public function getHasDefaultTimeSlots(): bool
1700 {
1701 return $this->has_default_time_slots ?? false;
1702 }
1703
1704 /**
1705 * Get default time slots as array
1706 */
1707 public function getDefaultTimeSlots(): array
1708 {
1709 if (empty($this->default_time_slots)) {
1710 return [];
1711 }
1712
1713 if (is_array($this->default_time_slots)) {
1714 return $this->default_time_slots;
1715 }
1716
1717 $decoded = json_decode($this->default_time_slots, true);
1718 return is_array($decoded) ? $decoded : [];
1719 }
1720
1721 /**
1722 * Get default departure time
1723 */
1724 public function getDepartureTime(): ?string
1725 {
1726 return $this->departure_time;
1727 }
1728
1729 /**
1730 * Get itinerary days
1731 */
1732 public function getItineraryDays(): array
1733 {
1734 return is_array($this->itinerary_days) ? $this->itinerary_days : [];
1735 }
1736
1737 /**
1738 * Get starting location
1739 */
1740 public function getStartingLocation(): ?string
1741 {
1742 return $this->starting_location;
1743 }
1744
1745 /**
1746 * Get starting latitude
1747 */
1748 public function getStartingLatitude(): ?string
1749 {
1750 return $this->starting_latitude ?? null;
1751 }
1752
1753 /**
1754 * Get starting longitude
1755 */
1756 public function getStartingLongitude(): ?string
1757 {
1758 return $this->starting_longitude ?? null;
1759 }
1760
1761 /**
1762 * Get seasonal availability
1763 */
1764 public function getSeasonalAvailability(): ?string
1765 {
1766 return $this->seasonal_availability;
1767 }
1768
1769 /**
1770 * Get available from date
1771 */
1772 public function getAvailableFrom(): ?string
1773 {
1774 return $this->available_from;
1775 }
1776
1777 /**
1778 * Get available to date
1779 */
1780 public function getAvailableTo(): ?string
1781 {
1782 return $this->available_to;
1783 }
1784
1785 /**
1786 * Get average rating
1787 */
1788 public function getAverageRating(): float
1789 {
1790 $a = (float) ($this->average_rating ?? 0);
1791 if ($a > 0) {
1792 return $a;
1793 }
1794
1795 return (float) ($this->avg_rating ?? 0);
1796 }
1797
1798 /**
1799 * Get review count
1800 */
1801 public function getReviewCount(): int
1802 {
1803 $n = (int) ($this->review_count ?? 0);
1804 if ($n > 0) {
1805 return $n;
1806 }
1807
1808 return (int) ($this->reviews_count ?? 0);
1809 }
1810
1811 /**
1812 * Get ID
1813 */
1814 public function getId(): int
1815 {
1816 return $this->id;
1817 }
1818
1819 /**
1820 * Get physical requirements
1821 */
1822 public function getPhysicalRequirements(): ?string
1823 {
1824 return $this->physical_requirements ?? null;
1825 }
1826
1827 /**
1828 * Get visa requirements
1829 */
1830 public function getVisaRequirements(): ?string
1831 {
1832 return $this->visa_requirements ?? null;
1833 }
1834
1835 /**
1836 * Get vaccination requirements
1837 */
1838 public function getVaccinationRequirements(): ?string
1839 {
1840 return $this->vaccination_requirements ?? null;
1841 }
1842
1843 /**
1844 * Get cancellation policy
1845 */
1846 public function getCancellationPolicy(): ?string
1847 {
1848 return $this->cancellation_policy ?? null;
1849 }
1850
1851 /**
1852 * Get trip story
1853 */
1854 public function getTripStory(): ?string
1855 {
1856 return $this->trip_story ?? null;
1857 }
1858
1859 /**
1860 * Get what makes special
1861 */
1862 public function getWhatMakesSpecial(): ?string
1863 {
1864 return $this->what_makes_special ?? null;
1865 }
1866
1867 /**
1868 * Get similar trips
1869 */
1870 public function getSimilarTrips(): array
1871 {
1872 return is_array($this->similar_trips ?? null) ? $this->similar_trips : [];
1873 }
1874
1875 /**
1876 * Get testimonials
1877 */
1878 public function getTestimonials(): array
1879 {
1880 return is_array($this->testimonials ?? null) ? $this->testimonials : [];
1881 }
1882
1883 /**
1884 * Create Trip model instance from stdClass object
1885 */
1886 public static function fromStdClass($stdObject): self
1887 {
1888 $trip = new self();
1889
1890 // Map all properties from stdClass to Trip model with proper type casting
1891 foreach (get_object_vars($stdObject) as $key => $value) {
1892 if (property_exists($trip, $key)) {
1893 // Cast values to proper types based on property declarations
1894 $trip->$key = self::castPropertyValue($key, $value);
1895 }
1896 }
1897
1898 return $trip;
1899 }
1900
1901 /**
1902 * Cast property values to correct types based on actual Trip model property definitions
1903 */
1904 private static function castPropertyValue(string $property, $value)
1905 {
1906 // Handle null/empty values - return null for nullable properties, defaults for non-nullable
1907 if ($value === null || $value === '') {
1908 // Check property type from actual model definition
1909 $reflection = new \ReflectionClass(self::class);
1910 if ($reflection->hasProperty($property)) {
1911 $propertyReflection = $reflection->getProperty($property);
1912 $type = $propertyReflection->getType();
1913
1914 if ($type && $type->allowsNull()) {
1915 return null; // Nullable property
1916 }
1917
1918 // Non-nullable property - return appropriate default
1919 if ($type instanceof \ReflectionNamedType) {
1920 switch ($type->getName()) {
1921 case 'int':
1922 return 0;
1923 case 'float':
1924 return 0.0;
1925 case 'bool':
1926 return false;
1927 case 'string':
1928 return '';
1929 case 'array':
1930 return [];
1931 default:
1932 return null;
1933 }
1934 }
1935 }
1936 return null; // Default for unknown properties
1937 }
1938
1939 // Cast non-null values to appropriate types
1940 $reflection = new \ReflectionClass(self::class);
1941 if ($reflection->hasProperty($property)) {
1942 $propertyReflection = $reflection->getProperty($property);
1943 $type = $propertyReflection->getType();
1944
1945 if ($type instanceof \ReflectionNamedType) {
1946 switch ($type->getName()) {
1947 case 'int':
1948 return (int) $value;
1949 case 'float':
1950 return (float) $value;
1951 case 'bool':
1952 return (bool) $value;
1953 case 'string':
1954 return (string) $value;
1955 case 'array':
1956 if (is_string($value)) {
1957 $decoded = maybe_unserialize($value);
1958 if (is_array($decoded)) {
1959 return $decoded;
1960 }
1961 $json = json_decode($value, true);
1962 return is_array($json) ? $json : [];
1963 }
1964 return is_array($value) ? $value : [];
1965 default:
1966 return $value;
1967 }
1968 }
1969 }
1970
1971 // Default string casting for unknown properties
1972 return (string) $value;
1973 }
1974 }
1975