PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 2.0.11 All 82 releases
yatra / app / Models / Trip.php

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

1,950 lines 73.4 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 (!empty($this->duration_days)) {
1139 $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null);
1140 }
1141
1142 return [
1143 'formatted' => $duration,
1144 'days' => $this->duration_days ?? 0,
1145 'nights' => $this->duration_nights ?? 0,
1146 'has_duration' => !empty($duration)
1147 ];
1148 }
1149
1150 /**
1151 * Get trip activities
1152 */
1153 public function getActivities(): array
1154 {
1155 // This will be populated by the repository when loading trip data
1156 return $this->activities ?? [];
1157 }
1158
1159 /**
1160 * Get trip image information
1161 */
1162 public function getImage(): array
1163 {
1164 $image_url = '';
1165 $has_image = false;
1166
1167 // Handle featured_image attachment ID
1168 if (!empty($this->featured_image) && is_numeric($this->featured_image)) {
1169 $attachment_url = wp_get_attachment_image_url((int) $this->featured_image, 'large');
1170 if ($attachment_url) {
1171 $image_url = $attachment_url;
1172 $has_image = true;
1173 }
1174 }
1175
1176 // Fallback to featured_image_url if it exists (for backward compatibility)
1177 if (empty($image_url) && !empty($this->featured_image_url)) {
1178 $image_url = $this->featured_image_url;
1179 $has_image = true;
1180 }
1181
1182 // Use placeholder SVG when no image exists
1183 if (empty($image_url)) {
1184 $image_url = plugins_url('assets/images/trip-placeholder.svg', YATRA_PLUGIN_FILE);
1185 $has_image = false;
1186 }
1187
1188 return [
1189 'url' => $image_url,
1190 'alt' => $this->getTitle(),
1191 'has_image' => $has_image
1192 ];
1193 }
1194
1195 /**
1196 * Get gallery image URLs from attachment IDs
1197 */
1198 public function getGalleryImageUrls(): array
1199 {
1200 $urls = [];
1201
1202 if (empty($this->gallery_images) || !is_array($this->gallery_images)) {
1203 return $urls;
1204 }
1205
1206 foreach ($this->gallery_images as $attachment_id) {
1207 if (is_numeric($attachment_id)) {
1208 $url = wp_get_attachment_image_url((int) $attachment_id, 'large');
1209 if ($url) {
1210 $urls[] = $url;
1211 }
1212 }
1213 }
1214
1215 return $urls;
1216 }
1217
1218 /**
1219 * Get trip permalink
1220 */
1221 public function getPermalink(): string
1222 {
1223 // If permalink is already set, use it
1224 if (!empty($this->permalink)) {
1225 return $this->permalink;
1226 }
1227
1228 // Use the proper helper function that handles trip base settings
1229 if (!empty($this->id)) {
1230 $permalink = yatra_get_trip_permalink($this);
1231
1232 // Debug logging for development
1233 if (defined('WP_DEBUG') && WP_DEBUG) {
1234 }
1235
1236 return $permalink;
1237 }
1238
1239 return '';
1240 }
1241
1242 /**
1243 * Check if trip is available for booking
1244 */
1245 public function isAvailableForBooking(): bool
1246 {
1247 // Check if trip is published
1248 if ($this->status !== 'publish') {
1249 return false;
1250 }
1251
1252 // Check if within available date range
1253 if (!empty($this->available_from) && strtotime($this->available_from) > time()) {
1254 return false;
1255 }
1256
1257 if (!empty($this->available_to) && strtotime($this->available_to) < time()) {
1258 return false;
1259 }
1260
1261 // Check capacity if specified
1262 if (!empty($this->max_travelers) && $this->max_travelers <= 0) {
1263 return false;
1264 }
1265
1266 return true;
1267 }
1268
1269 /**
1270 * Calculate effective price considering discounts
1271 */
1272 public function getEffectivePrice(): float
1273 {
1274 if ($this->pricing_type === 'traveler_based') {
1275 // Use centralized logic (default category if set, else minimum).
1276 return (float) \Yatra\Services\TripPricingService::getEffectivePrice((object) $this);
1277 }
1278
1279 // Use discounted price if available and valid
1280 if (!empty($this->discounted_price) && $this->discounted_price < $this->original_price) {
1281 return $this->discounted_price;
1282 }
1283
1284 // Use sale price if available and valid
1285 if (!empty($this->sale_price) && $this->sale_price < $this->original_price) {
1286 return $this->sale_price;
1287 }
1288
1289 return $this->original_price;
1290 }
1291
1292 /**
1293 * Check if trip has active discount
1294 */
1295 public function hasDiscount(): bool
1296 {
1297 if ($this->pricing_type === 'traveler_based') {
1298 return false; // Traveler-based pricing doesn't have simple discounts
1299 }
1300
1301 $effectivePrice = $this->getEffectivePrice();
1302 return $effectivePrice < $this->original_price;
1303 }
1304
1305 /**
1306 * Calculate discount percentage
1307 */
1308 public function getDiscountPercentage(): int
1309 {
1310 if (!$this->hasDiscount()) {
1311 return 0;
1312 }
1313
1314 $effectivePrice = $this->getEffectivePrice();
1315 $discount = (($this->original_price - $effectivePrice) / $this->original_price) * 100;
1316
1317 return (int) round($discount);
1318 }
1319
1320 /**
1321 * Check if deposit is required
1322 */
1323 public function requiresDeposit(): bool
1324 {
1325 return $this->deposit_required && ($this->deposit_amount > 0 || $this->deposit_percentage > 0);
1326 }
1327
1328 /**
1329 * Calculate deposit amount
1330 */
1331 public function getDepositAmount(): float
1332 {
1333 if (!$this->requiresDeposit()) {
1334 return 0.0;
1335 }
1336
1337 if ($this->deposit_amount > 0) {
1338 return $this->deposit_amount;
1339 }
1340
1341 if ($this->deposit_percentage > 0) {
1342 $effectivePrice = $this->getEffectivePrice();
1343 return ($effectivePrice * $this->deposit_percentage) / 100;
1344 }
1345
1346 return 0.0;
1347 }
1348
1349 /**
1350 * Check if trip is featured
1351 */
1352 public function isFeatured(): bool
1353 {
1354 return $this->featured_priority !== 'none' && !empty($this->featured_priority);
1355 }
1356
1357 /**
1358 * Get trip difficulty information
1359 */
1360 public function getDifficultyInfo(): array
1361 {
1362 return [
1363 'level' => $this->difficulty_level ?? 'moderate',
1364 'name' => $this->difficulty_name ?? ucfirst($this->difficulty_level ?? 'Moderate'),
1365 'icon' => $this->difficulty_icon ?? '',
1366 'has_difficulty' => !empty($this->difficulty_level)
1367 ];
1368 }
1369
1370 /**
1371 * Check if trip supports group bookings
1372 */
1373 public function supportsGroupBookings(): bool
1374 {
1375 return $this->group_pricing_enabled &&
1376 !empty($this->group_size_min) &&
1377 $this->group_size_min > 1;
1378 }
1379
1380 /**
1381 * Calculate group discount for given size
1382 */
1383 public function getGroupDiscount(int $groupSize): float
1384 {
1385 if (!$this->supportsGroupBookings() || $groupSize < $this->group_size_min) {
1386 return 0.0;
1387 }
1388
1389 if (!empty($this->group_size_max) && $groupSize > $this->group_size_max) {
1390 $groupSize = $this->group_size_max; // Cap at maximum
1391 }
1392
1393 if ($this->group_discount_type === 'percentage' && !empty($this->group_discount_percentage)) {
1394 return $this->group_discount_percentage;
1395 }
1396
1397 if ($this->group_discount_type === 'amount' && !empty($this->group_discount_amount)) {
1398 $effectivePrice = $this->getEffectivePrice();
1399 return ($this->group_discount_amount / $effectivePrice) * 100;
1400 }
1401
1402 return 0.0;
1403 }
1404
1405 /**
1406 * Validate trip data integrity
1407 */
1408 public function validate(): array
1409 {
1410 $errors = [];
1411
1412 // Required fields
1413 if (empty($this->title)) {
1414 $errors['title'] = 'Trip title is required';
1415 }
1416
1417 if (empty($this->slug)) {
1418 $errors['slug'] = 'Trip slug is required';
1419 }
1420
1421 // Pricing validation
1422 if ($this->original_price <= 0) {
1423 $errors['original_price'] = 'Original price must be greater than zero';
1424 }
1425
1426 if (!empty($this->discounted_price) && $this->discounted_price >= $this->original_price) {
1427 $errors['discounted_price'] = 'Discounted price must be less than original price';
1428 }
1429
1430 // Duration validation
1431 if ($this->trip_type === 'single_day' && $this->duration_days !== 1) {
1432 $errors['duration_days'] = 'Single day trips must have duration of 1 day';
1433 }
1434
1435 if ($this->trip_type === 'multi_day' && (!empty($this->duration_days) && $this->duration_days < 2)) {
1436 $errors['duration_days'] = 'Multi-day trips must have duration of at least 2 days';
1437 }
1438
1439 // Capacity validation
1440 if (!empty($this->max_travelers) && $this->max_travelers <= 0) {
1441 $errors['max_travelers'] = 'Maximum travelers must be greater than zero';
1442 }
1443
1444 // Group pricing validation
1445 if ($this->group_pricing_enabled) {
1446 if (empty($this->group_size_min) || $this->group_size_min <= 1) {
1447 $errors['group_size_min'] = 'Group minimum size must be greater than 1';
1448 }
1449
1450 if (!empty($this->group_size_max) && $this->group_size_max < $this->group_size_min) {
1451 $errors['group_size_max'] = 'Group maximum size must be greater than minimum size';
1452 }
1453 }
1454
1455 return $errors;
1456 }
1457
1458 /**
1459 * Get trip status information
1460 */
1461 public function getStatusInfo(): array
1462 {
1463 $statusLabels = [
1464 'draft' => 'Draft',
1465 'publish' => 'Published',
1466 'private' => 'Private',
1467 'trash' => 'Trashed'
1468 ];
1469
1470 return [
1471 'status' => $this->status,
1472 'label' => $statusLabels[$this->status] ?? 'Unknown',
1473 'is_published' => $this->status === 'publish',
1474 'is_draft' => $this->status === 'draft'
1475 ];
1476 }
1477
1478 /**
1479 * Get description
1480 */
1481 public function getDescription(): string
1482 {
1483 return $this->description ?? '';
1484 }
1485
1486 /**
1487 * Get highlights
1488 */
1489 public function getHighlights(): array
1490 {
1491 return is_array($this->highlights) ? $this->highlights : [];
1492 }
1493
1494 /**
1495 * Get FAQs
1496 */
1497 public function getFaqs(): array
1498 {
1499 return is_array($this->faqs) ? $this->faqs : [];
1500 }
1501
1502 /**
1503 * Get included items
1504 */
1505 public function getIncludedItems(): array
1506 {
1507 return is_array($this->included_items) ? $this->included_items : [];
1508 }
1509
1510 /**
1511 * Get excluded items
1512 */
1513 public function getExcludedItems(): array
1514 {
1515 return is_array($this->excluded_items) ? $this->excluded_items : [];
1516 }
1517
1518 /**
1519 * Get downloadable items
1520 */
1521 public function getDownloadableItems(): array
1522 {
1523 return is_array($this->downloadable_items) ? $this->downloadable_items : [];
1524 }
1525
1526 /**
1527 * Get landmarks
1528 */
1529 public function getLandmarks(): array
1530 {
1531 return is_array($this->landmarks) ? $this->landmarks : [];
1532 }
1533
1534 /**
1535 * Get attributes
1536 */
1537 public function getAttributes(): array
1538 {
1539 return is_array($this->attributes) ? $this->attributes : [];
1540 }
1541
1542 /**
1543 * Get trip type
1544 */
1545 public function getTripType(): string
1546 {
1547 return $this->trip_type ?? 'multiple_days';
1548 }
1549
1550 /**
1551 * Get duration days
1552 */
1553 public function getDurationDays(): int
1554 {
1555 return $this->duration_days ?? 0;
1556 }
1557
1558 /**
1559 * Get duration nights
1560 */
1561 public function getDurationNights(): int
1562 {
1563 return $this->duration_nights ?? 0;
1564 }
1565
1566 /**
1567 * Raw difficulty classification ID on the trip row (not a human-readable label).
1568 * For display, use {@see self::getDifficulty()} which resolves the name from the classifications table.
1569 */
1570 public function getDifficultyLevel(): string
1571 {
1572 return $this->difficulty_level ?? '';
1573 }
1574
1575 /**
1576 * Get min travelers
1577 */
1578 public function getMinTravelers(): int
1579 {
1580 return $this->min_travelers ?? 1;
1581 }
1582
1583 /**
1584 * Get max travelers
1585 */
1586 public function getMaxTravelers(): int
1587 {
1588 return $this->max_travelers ?? 20;
1589 }
1590
1591 /**
1592 * Get original price
1593 */
1594 public function getOriginalPrice(): float
1595 {
1596 if ($this->pricing_type === 'traveler_based') {
1597 $pricing = \Yatra\Services\TripPricingService::resolveDisplayPricing((object) $this);
1598 return (float) ($pricing['min_category_original_price'] ?? 0);
1599 }
1600 return (float) ($this->original_price ?? 0);
1601 }
1602
1603 /**
1604 * Get sale price
1605 */
1606 public function getSalePrice(): float
1607 {
1608 if ($this->pricing_type === 'traveler_based') {
1609 $pricing = \Yatra\Services\TripPricingService::resolveDisplayPricing((object) $this);
1610 return (float) ($pricing['effective_price_min'] ?? 0);
1611 }
1612 return (float) ($this->sale_price ?? 0);
1613 }
1614
1615 /**
1616 * Get price (alias for getEffectivePrice for backward compatibility)
1617 */
1618 public function getPrice(): float
1619 {
1620 return $this->getEffectivePrice();
1621 }
1622
1623 /**
1624 * Get price types
1625 */
1626 public function getPriceTypes(): array
1627 {
1628 return is_array($this->price_types) ? $this->price_types : [];
1629 }
1630
1631 /**
1632 * Get pricing type
1633 */
1634 public function getPricingType(): string
1635 {
1636 return $this->pricing_type ?? 'regular';
1637 }
1638
1639 /**
1640 * Get availability dates
1641 */
1642 public function getAvailabilityDates(): array
1643 {
1644 return is_array($this->availability_dates) ? $this->availability_dates : [];
1645 }
1646
1647 /**
1648 * Get booking mode
1649 */
1650 public function getBookingMode(): string
1651 {
1652 return $this->booking_mode ?? 'flexible';
1653 }
1654
1655 /**
1656 * Check if trip has specific availability configured
1657 */
1658 public function getHasSpecificAvailability(): bool
1659 {
1660 return $this->has_specific_availability ?? false;
1661 }
1662
1663 /**
1664 * Check if trip has booking capability
1665 */
1666 public function getHasBookingCapability(): bool
1667 {
1668 return $this->has_booking_capability ?? true;
1669 }
1670
1671 /**
1672 * Check if trip has default time slots enabled
1673 */
1674 public function getHasDefaultTimeSlots(): bool
1675 {
1676 return $this->has_default_time_slots ?? false;
1677 }
1678
1679 /**
1680 * Get default time slots as array
1681 */
1682 public function getDefaultTimeSlots(): array
1683 {
1684 if (empty($this->default_time_slots)) {
1685 return [];
1686 }
1687
1688 if (is_array($this->default_time_slots)) {
1689 return $this->default_time_slots;
1690 }
1691
1692 $decoded = json_decode($this->default_time_slots, true);
1693 return is_array($decoded) ? $decoded : [];
1694 }
1695
1696 /**
1697 * Get default departure time
1698 */
1699 public function getDepartureTime(): ?string
1700 {
1701 return $this->departure_time;
1702 }
1703
1704 /**
1705 * Get itinerary days
1706 */
1707 public function getItineraryDays(): array
1708 {
1709 return is_array($this->itinerary_days) ? $this->itinerary_days : [];
1710 }
1711
1712 /**
1713 * Get starting location
1714 */
1715 public function getStartingLocation(): ?string
1716 {
1717 return $this->starting_location;
1718 }
1719
1720 /**
1721 * Get starting latitude
1722 */
1723 public function getStartingLatitude(): ?string
1724 {
1725 return $this->starting_latitude ?? null;
1726 }
1727
1728 /**
1729 * Get starting longitude
1730 */
1731 public function getStartingLongitude(): ?string
1732 {
1733 return $this->starting_longitude ?? null;
1734 }
1735
1736 /**
1737 * Get seasonal availability
1738 */
1739 public function getSeasonalAvailability(): ?string
1740 {
1741 return $this->seasonal_availability;
1742 }
1743
1744 /**
1745 * Get available from date
1746 */
1747 public function getAvailableFrom(): ?string
1748 {
1749 return $this->available_from;
1750 }
1751
1752 /**
1753 * Get available to date
1754 */
1755 public function getAvailableTo(): ?string
1756 {
1757 return $this->available_to;
1758 }
1759
1760 /**
1761 * Get average rating
1762 */
1763 public function getAverageRating(): float
1764 {
1765 $a = (float) ($this->average_rating ?? 0);
1766 if ($a > 0) {
1767 return $a;
1768 }
1769
1770 return (float) ($this->avg_rating ?? 0);
1771 }
1772
1773 /**
1774 * Get review count
1775 */
1776 public function getReviewCount(): int
1777 {
1778 $n = (int) ($this->review_count ?? 0);
1779 if ($n > 0) {
1780 return $n;
1781 }
1782
1783 return (int) ($this->reviews_count ?? 0);
1784 }
1785
1786 /**
1787 * Get ID
1788 */
1789 public function getId(): int
1790 {
1791 return $this->id;
1792 }
1793
1794 /**
1795 * Get physical requirements
1796 */
1797 public function getPhysicalRequirements(): ?string
1798 {
1799 return $this->physical_requirements ?? null;
1800 }
1801
1802 /**
1803 * Get visa requirements
1804 */
1805 public function getVisaRequirements(): ?string
1806 {
1807 return $this->visa_requirements ?? null;
1808 }
1809
1810 /**
1811 * Get vaccination requirements
1812 */
1813 public function getVaccinationRequirements(): ?string
1814 {
1815 return $this->vaccination_requirements ?? null;
1816 }
1817
1818 /**
1819 * Get cancellation policy
1820 */
1821 public function getCancellationPolicy(): ?string
1822 {
1823 return $this->cancellation_policy ?? null;
1824 }
1825
1826 /**
1827 * Get trip story
1828 */
1829 public function getTripStory(): ?string
1830 {
1831 return $this->trip_story ?? null;
1832 }
1833
1834 /**
1835 * Get what makes special
1836 */
1837 public function getWhatMakesSpecial(): ?string
1838 {
1839 return $this->what_makes_special ?? null;
1840 }
1841
1842 /**
1843 * Get similar trips
1844 */
1845 public function getSimilarTrips(): array
1846 {
1847 return is_array($this->similar_trips ?? null) ? $this->similar_trips : [];
1848 }
1849
1850 /**
1851 * Get testimonials
1852 */
1853 public function getTestimonials(): array
1854 {
1855 return is_array($this->testimonials ?? null) ? $this->testimonials : [];
1856 }
1857
1858 /**
1859 * Create Trip model instance from stdClass object
1860 */
1861 public static function fromStdClass($stdObject): self
1862 {
1863 $trip = new self();
1864
1865 // Map all properties from stdClass to Trip model with proper type casting
1866 foreach (get_object_vars($stdObject) as $key => $value) {
1867 if (property_exists($trip, $key)) {
1868 // Cast values to proper types based on property declarations
1869 $trip->$key = self::castPropertyValue($key, $value);
1870 }
1871 }
1872
1873 return $trip;
1874 }
1875
1876 /**
1877 * Cast property values to correct types based on actual Trip model property definitions
1878 */
1879 private static function castPropertyValue(string $property, $value)
1880 {
1881 // Handle null/empty values - return null for nullable properties, defaults for non-nullable
1882 if ($value === null || $value === '') {
1883 // Check property type from actual model definition
1884 $reflection = new \ReflectionClass(self::class);
1885 if ($reflection->hasProperty($property)) {
1886 $propertyReflection = $reflection->getProperty($property);
1887 $type = $propertyReflection->getType();
1888
1889 if ($type && $type->allowsNull()) {
1890 return null; // Nullable property
1891 }
1892
1893 // Non-nullable property - return appropriate default
1894 if ($type instanceof \ReflectionNamedType) {
1895 switch ($type->getName()) {
1896 case 'int':
1897 return 0;
1898 case 'float':
1899 return 0.0;
1900 case 'bool':
1901 return false;
1902 case 'string':
1903 return '';
1904 case 'array':
1905 return [];
1906 default:
1907 return null;
1908 }
1909 }
1910 }
1911 return null; // Default for unknown properties
1912 }
1913
1914 // Cast non-null values to appropriate types
1915 $reflection = new \ReflectionClass(self::class);
1916 if ($reflection->hasProperty($property)) {
1917 $propertyReflection = $reflection->getProperty($property);
1918 $type = $propertyReflection->getType();
1919
1920 if ($type instanceof \ReflectionNamedType) {
1921 switch ($type->getName()) {
1922 case 'int':
1923 return (int) $value;
1924 case 'float':
1925 return (float) $value;
1926 case 'bool':
1927 return (bool) $value;
1928 case 'string':
1929 return (string) $value;
1930 case 'array':
1931 if (is_string($value)) {
1932 $decoded = maybe_unserialize($value);
1933 if (is_array($decoded)) {
1934 return $decoded;
1935 }
1936 $json = json_decode($value, true);
1937 return is_array($json) ? $json : [];
1938 }
1939 return is_array($value) ? $value : [];
1940 default:
1941 return $value;
1942 }
1943 }
1944 }
1945
1946 // Default string casting for unknown properties
1947 return (string) $value;
1948 }
1949 }
1950