PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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.7, at app/Models/Trip.php

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