PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / includes / helpers.php

helpers.php in Yatra – Travel Booking & Tour Operator Software 3.0.3, at includes/helpers.php

2,837 lines 89.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Yatra Helper Functions
4 *
5 * @package Yatra
6 */
7
8 // Prevent direct access
9 if (!defined('ABSPATH')) {
10 exit;
11 }
12
13 use Yatra\Database\Tables\BookingsTable;
14 use Yatra\Database\Tables\ClassificationsTable;
15 use Yatra\Database\Tables\ReviewsTable;
16 use Yatra\Database\Tables\TripsTable;
17 use Yatra\Services\SettingsService;
18 use Yatra\Constants\ClassificationTypes;
19
20 /**
21 * Get a plugin setting value
22 *
23 * @param string $key Setting key
24 * @param mixed $default Default value
25 * @return mixed
26 */
27 function yatra_get_setting(string $key, $default = null)
28 {
29 return SettingsService::get($key, $default);
30 }
31
32 /**
33 * Check if a setting is enabled
34 *
35 * @param string $key Setting key
36 * @return bool
37 */
38 function yatra_setting_enabled(string $key): bool
39 {
40 return SettingsService::isEnabled($key);
41 }
42
43 /**
44 * Check if reviews are enabled
45 *
46 * @return bool
47 */
48 function yatra_reviews_enabled(): bool
49 {
50 return SettingsService::reviewsEnabled();
51 }
52
53 /**
54 * Get booking form configuration
55 *
56 * @return array
57 */
58 function yatra_get_booking_form_config(): array
59 {
60 // Check if Dynamic Form Field module is enabled via Pro plugin
61 $is_dynamic_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
62
63 if ($is_dynamic_enabled) {
64 // Pro module is active — merged config from options (filtered in SettingsService::getBookingFormConfig)
65 return SettingsService::getBookingFormConfig();
66 }
67
68 // Module off: still allow filters to adjust defaults (tests / edge integrations)
69 return apply_filters(
70 'yatra_booking_form_config',
71 SettingsService::getDefaultBookingFormConfig()
72 );
73 }
74
75 /**
76 * Check if user can leave a review for a trip
77 *
78 * @param int $trip_id Trip ID
79 * @param int|null $user_id User ID (defaults to current user)
80 * @return bool
81 */
82 function yatra_can_review(int $trip_id, ?int $user_id = null): bool
83 {
84 // Reviews must be enabled
85 if (!SettingsService::reviewsEnabled()) {
86 return false;
87 }
88
89 // Get user ID
90 if ($user_id === null) {
91 $user_id = get_current_user_id();
92 }
93
94 // If booking required, check if user has booked this trip
95 if (SettingsService::requireBookingForReview()) {
96 if ($user_id === 0) {
97 return false; // Guest can't review if booking required
98 }
99
100 // Check if user has a completed booking for this trip
101 global $wpdb;
102 $table = BookingsTable::getTableName();
103 $has_booking = $wpdb->get_var($wpdb->prepare(
104 "SELECT COUNT(*) FROM {$table}
105 WHERE trip_id = %d AND customer_id = %d AND status = 'completed'",
106 $trip_id,
107 $user_id
108 ));
109
110 if (!$has_booking) {
111 return false;
112 }
113 }
114
115 // Check if user already reviewed this trip (but allow if within edit window)
116 if ($user_id > 0) {
117 $existing_review = yatra_get_user_review($trip_id, $user_id);
118 if ($existing_review && !yatra_can_edit_review($existing_review)) {
119 return false;
120 }
121 }
122
123 return true;
124 }
125
126 /**
127 * Get user's existing review for a trip
128 *
129 * @param int $trip_id Trip ID
130 * @param int|null $user_id User ID (defaults to current user)
131 * @return object|null Review object or null
132 */
133 function yatra_get_user_review(int $trip_id, ?int $user_id = null): ?object
134 {
135 if ($user_id === null) {
136 $user_id = get_current_user_id();
137 }
138
139 if ($user_id === 0) {
140 return null;
141 }
142
143 global $wpdb;
144 $table = ReviewsTable::getTableName();
145 $review = $wpdb->get_row($wpdb->prepare(
146 "SELECT * FROM {$table} WHERE trip_id = %d AND user_id = %d ORDER BY created_at DESC LIMIT 1",
147 $trip_id,
148 $user_id
149 ));
150
151 return $review ?: null;
152 }
153
154 /**
155 * Check if a review can be edited (within 24 hours of creation and not approved)
156 *
157 * @param object $review Review object with created_at and status fields
158 * @return bool
159 */
160 function yatra_can_edit_review(object $review): bool
161 {
162 if (empty($review->created_at)) {
163 return false;
164 }
165
166 // Don't allow editing if review is approved
167 if (isset($review->status) && $review->status === 'approved') {
168 return false;
169 }
170
171 $created_time = strtotime($review->created_at);
172 $current_time = current_time('timestamp');
173 $hours_since_creation = ($current_time - $created_time) / 3600;
174
175 // Allow editing within 24 hours (only for pending/rejected reviews)
176 return $hours_since_creation <= 24;
177 }
178
179 /**
180 * Get time remaining to edit a review
181 *
182 * @param object $review Review object with created_at field
183 * @return string Human-readable time remaining (e.g., "5 hours", "30 minutes")
184 */
185 function yatra_get_review_edit_time_remaining(object $review): string
186 {
187 if (empty($review->created_at)) {
188 return '';
189 }
190
191 $created_time = strtotime($review->created_at);
192 $current_time = current_time('timestamp');
193 $seconds_since_creation = $current_time - $created_time;
194 $seconds_remaining = (24 * 3600) - $seconds_since_creation;
195
196 if ($seconds_remaining <= 0) {
197 return '';
198 }
199
200 $hours = floor($seconds_remaining / 3600);
201 $minutes = floor(($seconds_remaining % 3600) / 60);
202
203 if ($hours > 0) {
204 return sprintf(_n('%d hour', '%d hours', $hours, 'yatra'), $hours);
205 }
206
207 return sprintf(_n('%d minute', '%d minutes', $minutes, 'yatra'), $minutes);
208 }
209
210 /**
211 * Get the booking URL for a trip
212 *
213 * @param string $trip_slug The trip slug
214 * @param array $params Optional URL parameters (date, adults, children, price)
215 * @return string The booking URL
216 */
217 function yatra_get_booking_url(string $trip_slug, array $params = []): string
218 {
219 $permalink_structure = get_option('permalink_structure');
220 $is_plain = empty($permalink_structure);
221
222 // Check if using custom booking page via SettingsService
223 if (SettingsService::useCustomBookingPage()) {
224 $page_url = get_permalink(SettingsService::getBookingPageId());
225 if ($page_url) {
226 $params['trip'] = $trip_slug;
227 return add_query_arg($params, $page_url);
228 }
229 }
230
231 // Using default dynamic URL
232 $booking_base = SettingsService::getBookingBase();
233 if ($is_plain) {
234 $params['trip'] = $trip_slug;
235
236 return add_query_arg(
237 array_merge(['yatra_page' => $booking_base], $params),
238 home_url('/')
239 );
240 }
241
242 $url = home_url('/' . $booking_base . '/' . $trip_slug);
243
244 if (!empty($params)) {
245 $url = add_query_arg($params, $url);
246 }
247
248 return $url;
249 }
250
251 /**
252 * Normalized Dynamic Pricing display toggles (listing, trip page, availability).
253 *
254 * @return array{show_original_price: bool, show_savings_badge: bool, show_urgency_messages: bool}
255 */
256 if (!function_exists('yatra_get_dynamic_pricing_display_flags')) {
257 function yatra_get_dynamic_pricing_display_flags(): array
258 {
259 $s = apply_filters('yatra_get_dynamic_pricing_display_settings', [
260 'show_original_price' => true,
261 'show_savings_badge' => true,
262 'show_urgency_messages' => false,
263 ]);
264
265 return [
266 'show_original_price' => filter_var($s['show_original_price'] ?? true, FILTER_VALIDATE_BOOLEAN),
267 'show_savings_badge' => filter_var($s['show_savings_badge'] ?? true, FILTER_VALIDATE_BOOLEAN),
268 'show_urgency_messages' => filter_var($s['show_urgency_messages'] ?? false, FILTER_VALIDATE_BOOLEAN),
269 ];
270 }
271 }
272
273 /**
274 * Urgency lines for a trip surface (listing card, sidebar, similar trips). Pro fills via yatra_trip_card_dynamic_pricing_meta.
275 *
276 * @param array<string, mixed> $context base_sale_price, base_original_price, departure_date, spots_remaining, …
277 * @return array<int, string>
278 */
279 if (!function_exists('yatra_trip_card_dynamic_pricing_urgency_lines')) {
280 function yatra_trip_card_dynamic_pricing_urgency_lines(int $trip_id, array $context = []): array
281 {
282 if ($trip_id <= 0) {
283 return [];
284 }
285
286 $flags = yatra_get_dynamic_pricing_display_flags();
287 if (!$flags['show_urgency_messages']) {
288 return [];
289 }
290
291 $meta = apply_filters(
292 'yatra_trip_card_dynamic_pricing_meta',
293 ['urgency_messages' => []],
294 array_merge($context, [
295 'trip_id' => $trip_id,
296 'display' => $flags,
297 ])
298 );
299
300 if (!is_array($meta) || empty($meta['urgency_messages']) || !is_array($meta['urgency_messages'])) {
301 return [];
302 }
303
304 $out = [];
305 foreach ($meta['urgency_messages'] as $line) {
306 $line = sanitize_text_field((string) $line);
307 if ($line !== '') {
308 $out[] = $line;
309 }
310 }
311
312 return array_values(array_unique($out));
313 }
314 }
315
316 /**
317 * Format price with currency
318 *
319 * @param float $amount The amount to format
320 * @param string|null $currency The currency code (optional, uses global setting if not provided)
321 * @param bool $zero_is_unknown When true (default), 0 is shown as "Contact for pricing" (trip/listing).
322 * Set false for checkout, payments, and invoices where 0 is a real amount.
323 * @return string Formatted price
324 */
325 if (!function_exists('yatra_format_price')) {
326 function yatra_format_price(float $amount, ?string $currency = null, bool $zero_is_unknown = true): string
327 {
328 if ($zero_is_unknown && (empty($amount) || $amount == 0)) {
329 return __('Contact for pricing', 'yatra');
330 }
331
332 // Get currency from global settings if not provided
333 if (empty($currency)) {
334 $currency = SettingsService::getCurrency();
335 }
336
337 // Get formatting settings from global settings
338 $currency_position = SettingsService::getCurrencyPosition();
339 $decimal_places = SettingsService::getInt('decimal_places', 2);
340 // Avoid absurd migrated values (e.g. 7+) breaking storefront display; cap at 4.
341 $decimal_places = max(0, min(4, $decimal_places));
342 $thousand_separator = SettingsService::getString('thousand_separator', ',');
343 $decimal_separator = SettingsService::getString('decimal_separator', '.');
344
345 // Format the amount with proper separators
346 $formatted_amount = number_format($amount, $decimal_places, $decimal_separator, $thousand_separator);
347
348 // Get currency symbol
349 $currency_symbol = yatra_get_currency_symbol($currency);
350
351 // Placement: Settings UI uses left, right, left_space, right_space; legacy uses before/after.
352 $raw = strtolower(trim((string) $currency_position));
353 if ($raw === 'before') {
354 $pos = 'left_space';
355 } elseif ($raw === 'after') {
356 $pos = 'right_space';
357 } else {
358 $pos = $raw;
359 }
360 $allowed = ['left', 'right', 'left_space', 'right_space'];
361 if (!in_array($pos, $allowed, true)) {
362 $pos = 'left_space';
363 }
364
365 if ($pos === 'right') {
366 return $formatted_amount . $currency_symbol;
367 }
368 if ($pos === 'right_space') {
369 return $formatted_amount . ' ' . $currency_symbol;
370 }
371 if ($pos === 'left') {
372 return $currency_symbol . $formatted_amount;
373 }
374
375 return $currency_symbol . ' ' . $formatted_amount;
376 }
377 }
378
379 /**
380 * Get currency symbol from currency code
381 *
382 * @param string $currency_code The currency code (e.g., 'USD', 'EUR', 'NPR')
383 * @return string The currency symbol or code
384 */
385 if (!function_exists('yatra_get_currency_symbol')) {
386 function yatra_get_currency_symbol(string $currency_code): string
387 {
388 $symbols = [
389 'USD' => '$',
390 'EUR' => '',
391 'GBP' => '£',
392 'JPY' => '¥',
393 'CNY' => '¥',
394 'INR' => '',
395 'NPR' => 'Rs',
396 'AUD' => 'A$',
397 'CAD' => 'C$',
398 'CHF' => 'CHF',
399 'NZD' => 'NZ$',
400 'SGD' => 'S$',
401 'HKD' => 'HK$',
402 'KRW' => '',
403 'THB' => '฿',
404 'MYR' => 'RM',
405 'PHP' => '',
406 'IDR' => 'Rp',
407 'VND' => '',
408 'BRL' => 'R$',
409 'MXN' => 'MX$',
410 'RUB' => '',
411 'ZAR' => 'R',
412 'AED' => 'د.إ',
413 'SAR' => '',
414 'TRY' => '',
415 'SEK' => 'kr',
416 'NOK' => 'kr',
417 'DKK' => 'kr',
418 'PLN' => '',
419 'CZK' => '',
420 'HUF' => 'Ft',
421 'ILS' => '',
422 'TWD' => 'NT$',
423 'PKR' => '',
424 'BDT' => '',
425 'LKR' => 'Rs',
426 'EGP' => '',
427 'NGN' => '',
428 'KES' => 'KSh',
429 // Ghanaian cedi – use plain symbol without the GH prefix
430 'GHS' => '',
431 'GHC' => '',
432 'ARS' => 'AR$',
433 'CLP' => 'CL$',
434 'COP' => 'CO$',
435 'PEN' => 'S/',
436 ];
437
438 return $symbols[strtoupper($currency_code)] ?? $currency_code;
439 }
440 }
441
442 /**
443 * Format duration (days/nights)
444 *
445 * @param int $days Number of days
446 * @param int|null $nights Number of nights (optional)
447 * @return string Formatted duration
448 */
449 if (!function_exists('yatra_format_duration')) {
450 function yatra_format_duration(int $days, ?int $nights = null): string
451 {
452 if ($days > 0 && $nights !== null && $nights > 0) {
453 /* translators: 1: days count, 2: nights count */
454 return sprintf(__('%1$d days / %2$d nights', 'yatra'), $days, $nights);
455 }
456 if ($days > 0) {
457 return sprintf(
458 _n('%d day', '%d days', $days, 'yatra'),
459 $days
460 );
461 }
462 return __('Flexible', 'yatra');
463 }
464 }
465
466 /**
467 * Render SVG icon
468 *
469 * @param string $icon_name Icon name
470 * @param string $class Optional CSS class
471 * @return string SVG markup
472 */
473 if (!function_exists('yatra_svg_icon')) {
474 function yatra_svg_icon(string $icon_name, string $class = ''): string
475 {
476 static $icons = null;
477
478 // Load icons from JSON file once
479 if ($icons === null) {
480 $icons_file = dirname(__FILE__) . '/icons.json';
481 if (file_exists($icons_file)) {
482 $icons_data = json_decode(file_get_contents($icons_file), true);
483 $icons = [];
484
485 // Convert JSON data to PHP array
486 foreach ($icons_data as $name => $data) {
487 if (isset($data['svg'])) {
488 $icons[$name] = (string) $data['svg'];
489 }
490 }
491 } else {
492 $icons = [];
493 }
494 }
495
496 $svg = $icons[$icon_name] ?? '';
497
498 if ($svg === '' || !is_string($svg)) {
499 return '';
500 }
501
502 if ($class !== '') {
503 $class_attr = esc_attr($class);
504
505 if (preg_match('/<svg[^>]*\sclass="([^"]*)"/i', $svg, $m)) {
506 $existing = trim((string) ($m[1] ?? ''));
507 $merged = trim($existing . ' ' . $class_attr);
508 $svg = preg_replace('/(<svg[^>]*\sclass=")([^"]*)(")/i', '$1' . $merged . '$3', $svg, 1);
509 } else {
510 $svg = preg_replace('/<svg\b/i', '<svg class="' . $class_attr . '"', $svg, 1);
511 }
512 }
513
514 return (string) $svg;
515 }
516 }
517
518 /**
519 * Allowed Font Awesome Free icon name (maps to fa-{name} class).
520 */
521 if (!function_exists('yatra_sanitize_fa_icon_slug')) {
522 function yatra_sanitize_fa_icon_slug(string $name): string
523 {
524 $n = strtolower(trim($name));
525 if ($n === '' || strlen($n) > 64) {
526 return '';
527 }
528 if (!preg_match('/^[a-z0-9-]+$/', $n)) {
529 return '';
530 }
531
532 return $n;
533 }
534 }
535
536 /**
537 * Normalize icon picker payload before storing (REST / services).
538 *
539 * @param array<string, mixed> $icon
540 * @return array{type: string, value: string|int, provider?: string}
541 */
542 if (!function_exists('yatra_normalize_icon_picker_for_storage')) {
543 function yatra_normalize_icon_picker_for_storage(array $icon): array
544 {
545 $type = isset($icon['type']) && $icon['type'] === 'image' ? 'image' : 'icon';
546 $value = $icon['value'] ?? '';
547 if ($type === 'image') {
548 return [
549 'type' => 'image',
550 'value' => is_numeric($value) ? (int) $value : sanitize_text_field((string) $value),
551 ];
552 }
553 $provider = isset($icon['provider']) ? sanitize_key((string) $icon['provider']) : 'yatra';
554 if (!in_array($provider, ['yatra', 'fa-solid', 'fa-regular'], true)) {
555 $provider = 'yatra';
556 }
557
558 return [
559 'type' => 'icon',
560 'value' => sanitize_text_field((string) $value),
561 'provider' => $provider,
562 ];
563 }
564 }
565
566 /**
567 * Markup for a stored icon picker value (Yatra SVG registry, Font Awesome, or image).
568 *
569 * @param array<string, mixed>|string|null $picker Serialized JSON string, array, or null.
570 * @return string Safe HTML (empty string if nothing renderable).
571 */
572 if (!function_exists('yatra_stored_picker_icon_markup')) {
573 function yatra_stored_picker_icon_markup($picker, string $default_yatra_slug = 'mountain', string $class = ''): string
574 {
575 $class = trim($class);
576 $class_attr = $class !== '' ? ' ' . esc_attr($class) : '';
577
578 if ($picker === null || $picker === '') {
579 return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : '';
580 }
581 if (is_string($picker) && strpos($picker, '{') === 0) {
582 $picker = json_decode($picker, true);
583 }
584 if (!is_array($picker) || !isset($picker['type'])) {
585 if (is_string($picker)) {
586 $slug = trim($picker);
587
588 return $slug !== '' && function_exists('yatra_svg_icon')
589 ? yatra_svg_icon($slug, $class)
590 : yatra_svg_icon($default_yatra_slug, $class);
591 }
592
593 return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : '';
594 }
595
596 if ($picker['type'] === 'image' && !empty($picker['value'])) {
597 $image_url = is_numeric($picker['value'])
598 ? wp_get_attachment_url((int) $picker['value'])
599 : (string) $picker['value'];
600 if ($image_url) {
601 $style = 'width:24px;height:24px;object-fit:cover;border-radius:4px;';
602
603 return '<img src="' . esc_url($image_url) . '" alt="" class="' . esc_attr(trim('yatra-picker-img-icon ' . $class)) . '" style="' . esc_attr($style) . '" loading="lazy" decoding="async" />';
604 }
605
606 return function_exists('yatra_svg_icon') ? yatra_svg_icon('image', $class) : '';
607 }
608
609 if ($picker['type'] === 'icon' && !empty($picker['value'])) {
610 $provider = isset($picker['provider']) ? sanitize_key((string) $picker['provider']) : 'yatra';
611 if ($provider === 'fa-solid' || $provider === 'fa-regular') {
612 $slug = yatra_sanitize_fa_icon_slug((string) $picker['value']);
613 if ($slug === '') {
614 return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : '';
615 }
616 $fa_prefix = $provider === 'fa-regular' ? 'fa-regular' : 'fa-solid';
617
618 return '<i class="' . esc_attr($fa_prefix . ' fa-' . $slug . $class_attr) . '" aria-hidden="true"></i>';
619 }
620
621 return function_exists('yatra_svg_icon')
622 ? yatra_svg_icon((string) $picker['value'], $class)
623 : '';
624 }
625
626 return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : '';
627 }
628 }
629
630 /**
631 * Translated display label for trip meal_plan stored slug (matches admin Trip Builder options).
632 *
633 * @param string|null $slug Raw value from DB (e.g. half_board, "Half Board").
634 */
635 if (!function_exists('yatra_meal_plan_label')) {
636 function yatra_meal_plan_label(?string $slug): string
637 {
638 if ($slug === null || $slug === '') {
639 return '';
640 }
641 $s = strtolower(trim(preg_replace('/[\s\-]+/', '_', $slug), " \t\n\r\0\x0B_-"));
642 switch ($s) {
643 case 'breakfast':
644 return __('Breakfast Only', 'yatra');
645 case 'half_board':
646 return __('Half Board (Breakfast + Dinner)', 'yatra');
647 case 'full_board':
648 return __('Full Board (All Meals)', 'yatra');
649 case 'all_inclusive':
650 return __('All Inclusive', 'yatra');
651 case 'none':
652 return __('No Meals Included', 'yatra');
653 default:
654 return ucwords(str_replace('_', ' ', $s));
655 }
656 }
657 }
658
659 /**
660 * Translated itinerary entry item type label for frontend (matches admin item type names).
661 */
662 if (!function_exists('yatra_itinerary_item_type_label')) {
663 function yatra_itinerary_item_type_label(string $type): string
664 {
665 $t = trim($type);
666 switch ($t) {
667 case 'Meal':
668 return __('Meal', 'yatra');
669 case 'Activity':
670 return __('Activity', 'yatra');
671 case 'Accommodation':
672 return __('Accommodation', 'yatra');
673 case 'Transportation':
674 return __('Transportation', 'yatra');
675 case 'Rest':
676 return __('Rest', 'yatra');
677 default:
678 return $t;
679 }
680 }
681 }
682
683 /**
684 * Extract SVG icon slug from a stored icon field (same shape as admin / archive cards).
685 *
686 * @param mixed $icon Raw value from DB (serialized array with type/value, URL, attachment id, or legacy slug string).
687 */
688 function yatra_icon_slug_from_stored_field($icon): string
689 {
690 if ($icon === null || $icon === '') {
691 return '';
692 }
693
694 $icon = maybe_unserialize($icon);
695
696 if (is_array($icon)) {
697 $type = $icon['type'] ?? $icon[0] ?? '';
698 $value = $icon['value'] ?? $icon[1] ?? '';
699 if ($type === 'icon' && !empty($value) && is_string($value)) {
700 $provider = isset($icon['provider']) ? sanitize_key((string) $icon['provider']) : 'yatra';
701 if ($provider === 'fa-solid' || $provider === 'fa-regular') {
702 return '';
703 }
704
705 return $value;
706 }
707
708 return '';
709 }
710
711 if (is_string($icon)) {
712 if (filter_var($icon, FILTER_VALIDATE_URL)) {
713 return '';
714 }
715 $slug = trim($icon);
716
717 return $slug !== '' ? $slug : '';
718 }
719
720 return '';
721 }
722
723 /**
724 * SVG markup for archive listing CTAs: use admin icon when present and valid in icons.json; else default slug.
725 *
726 * @param string $resolved_icon_slug From the listing loop (same source as card hero icon when type is "icon").
727 * @param string $default_slug icons.json key when no admin icon.
728 */
729 function yatra_archive_listing_cta_icon_markup(string $resolved_icon_slug, string $default_slug, string $class = 'yatra-btn-icon'): string
730 {
731 $slug = trim($resolved_icon_slug);
732 if ($slug !== '' && function_exists('yatra_svg_icon')) {
733 $out = yatra_svg_icon($slug, $class);
734 if ($out !== '') {
735 return $out;
736 }
737 }
738
739 $fallback = trim($default_slug);
740 if ($fallback !== '' && function_exists('yatra_svg_icon')) {
741 return yatra_svg_icon($fallback, $class);
742 }
743
744 return '';
745 }
746
747 /**
748 * Icon slug for trip listing card "View Details" — category, then destination, then difficulty (backend order).
749 *
750 * @param array<int, object|array<string, mixed>> $categories Trip categories from getCategories()
751 * @param array<int, object|array<string, mixed>> $destinations Trip destinations from getDestinations()
752 * @param array<string, mixed> $difficulty From Trip::getDifficulty()
753 */
754 function yatra_trip_listing_card_cta_icon_slug(array $categories, array $destinations, array $difficulty): string
755 {
756 foreach ($categories as $row) {
757 if (empty($row)) {
758 continue;
759 }
760 $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null);
761 $slug = yatra_icon_slug_from_stored_field($raw);
762 if ($slug !== '') {
763 return $slug;
764 }
765 }
766
767 foreach ($destinations as $row) {
768 if (empty($row)) {
769 continue;
770 }
771 $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null);
772 $slug = yatra_icon_slug_from_stored_field($raw);
773 if ($slug !== '') {
774 return $slug;
775 }
776 }
777
778 if (!empty($difficulty['icon']) && is_string($difficulty['icon'])) {
779 $try = trim($difficulty['icon']);
780 if ($try !== '') {
781 return $try;
782 }
783 }
784
785 return '';
786 }
787
788 /**
789 * Get booking base URL slug
790 *
791 * @return string The booking base slug
792 */
793 function yatra_get_booking_base(): string
794 {
795 // Check if using custom booking page
796 if (SettingsService::useCustomBookingPage()) {
797 $booking_page_id = SettingsService::getBookingPageId();
798 if ($booking_page_id > 0) {
799 $page = get_post($booking_page_id);
800 if ($page) {
801 return $page->post_name;
802 }
803 }
804 }
805
806 return SettingsService::getBookingBase();
807 }
808
809 /**
810 * Check if the current page is a booking page
811 *
812 * @return bool
813 */
814 function yatra_is_booking_page(): bool
815 {
816 global $wp_query;
817
818 // Check for custom booking page
819 if (SettingsService::useCustomBookingPage()) {
820 $booking_page_id = SettingsService::getBookingPageId();
821 if ($booking_page_id > 0 && is_page($booking_page_id)) {
822 return true;
823 }
824 }
825
826 $booking_base = SettingsService::getBookingBase();
827 if (!empty($wp_query->get('yatra_page')) && (string) $wp_query->get('yatra_page') === $booking_base) {
828 return true;
829 }
830
831 // Check for dynamic booking URL
832 return !empty($wp_query->get('yatra_booking_trip_slug'));
833 }
834
835 /**
836 * Get the global trip object
837 *
838 * Similar to WordPress get_post(), this function returns the current trip object
839 * when on a single trip page.
840 *
841 * @return object|null The trip object or null if not on a trip page
842 */
843 function yatra_get_trip(): ?object
844 {
845 global $trip;
846 return $trip ?? null;
847 }
848
849 /**
850 * Check if we're on a single trip page
851 *
852 * @return bool True if on a single trip page
853 */
854 function yatra_is_single_trip(): bool
855 {
856
857 global $wp_query;
858 return !empty($wp_query->get('yatra_trip_id'));
859 }
860
861 /**
862 * Get a trip field value with default fallback
863 *
864 * @param string $field The field name
865 * @param mixed $default Default value if field is empty
866 * @return mixed The field value or default
867 */
868 function yatra_get_trip_field(string $field, $default = '')
869 {
870 global $trip;
871
872 if (!$trip || !isset($trip->$field)) {
873 return $default;
874 }
875
876 return $trip->$field ?: $default;
877 }
878
879 /**
880 * Echo a trip field value with escaping
881 *
882 * @param string $field The field name
883 * @param string $escape Escape function: 'html', 'attr', 'url', 'js', 'none'
884 * @param mixed $default Default value if field is empty
885 */
886 function yatra_trip_field(string $field, string $escape = 'html', $default = ''): void
887 {
888 $value = yatra_get_trip_field($field, $default);
889
890 switch ($escape) {
891 case 'html':
892 echo esc_html($value);
893 break;
894 case 'attr':
895 echo esc_attr($value);
896 break;
897 case 'url':
898 echo esc_url($value);
899 break;
900 case 'js':
901 echo esc_js($value);
902 break;
903 case 'none':
904 case 'kses':
905 echo wp_kses_post($value);
906 break;
907 default:
908 echo esc_html($value);
909 }
910 }
911
912 /**
913 * Public URL for the Yatra brand icon (admin menu + React sidebar). Empty if file is missing.
914 */
915 function yatra_get_brand_icon_url(): string
916 {
917 if (!defined('YATRA_PLUGIN_PATH') || !defined('YATRA_PLUGIN_URL')) {
918 return '';
919 }
920
921 $candidates = [
922 'assets/images/yatra-icon.png',
923 'assets/images/yara-icon.png',
924 ];
925
926 foreach ($candidates as $relative) {
927 $file = YATRA_PLUGIN_PATH . $relative;
928 if (!is_readable($file)) {
929 continue;
930 }
931
932 $url = YATRA_PLUGIN_URL . $relative;
933
934 return add_query_arg('ver', (string) filemtime($file), $url);
935 }
936
937 return '';
938 }
939
940 /**
941 * ============================================
942 * BOOKING SESSION MANAGEMENT
943 * ============================================
944 */
945
946 /**
947 * Start WordPress session if not already started
948 */
949 function yatra_start_session(): void
950 {
951 // Start output buffering to prevent accidental output from breaking sessions
952 if (!ob_get_level()) {
953 ob_start();
954 }
955
956 if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
957 // Set session cookie parameters for better compatibility
958 if (PHP_VERSION_ID >= 70300) {
959 session_set_cookie_params([
960 'lifetime' => 0,
961 'path' => defined('COOKIEPATH') ? COOKIEPATH : '/',
962 'domain' => defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '',
963 'secure' => is_ssl(),
964 'httponly' => true,
965 'samesite' => 'Lax'
966 ]);
967 }
968 session_start();
969 }
970 }
971
972 /**
973 * Set booking session data
974 *
975 * @param array $data Booking data to store
976 */
977 function yatra_set_booking_session(array $data): void
978 {
979 yatra_start_session();
980
981 // Clear any existing remaining payment session to avoid conflicts
982 unset($_SESSION['yatra_remaining']);
983
984 $session_data = array_merge(
985 $_SESSION['yatra_booking'] ?? [],
986 $data,
987 ['timestamp' => time()]
988 );
989
990 $_SESSION['yatra_booking'] = $session_data;
991
992 // ALWAYS store in transient as backup (not just for REST API)
993 // This ensures data persists across all request types
994 // Generate or reuse booking token
995 $booking_token = $_SESSION['yatra_booking_token'] ?? 'yatra_booking_' . wp_generate_password(32, false);
996 $_SESSION['yatra_booking_token'] = $booking_token;
997 $session_data['booking_token'] = $booking_token;
998
999 // Store in transient (expires in 30 minutes)
1000 try {
1001 $transient_set = set_transient($booking_token, $session_data, 1800);
1002 } catch (Exception $e) {
1003 // Continue without transient - session fallback will be used
1004 }
1005
1006 }
1007
1008 /**
1009 * Get booking session data
1010 *
1011 * @param string|null $key Specific key to retrieve, or null for all data
1012 * @param mixed $default Default value if key not found
1013 * @return mixed
1014 */
1015 function yatra_get_booking_session(?string $key = null, $default = null)
1016 {
1017 yatra_start_session();
1018
1019 $booking_data = $_SESSION['yatra_booking'] ?? [];
1020
1021 // If session is empty, try to restore from transient (REST API → page load transition)
1022 if (empty($booking_data) || empty($booking_data['trip_id'])) {
1023 // Check for booking token in URL or session
1024 $booking_token = $_GET['booking_token'] ?? $_SESSION['yatra_booking_token'] ?? null;
1025
1026 if ($booking_token) {
1027 try {
1028 $transient_data = get_transient($booking_token);
1029
1030 if ($transient_data && is_array($transient_data) && !empty($transient_data['trip_id'])) {
1031 // Validate transient data integrity
1032 if (isset($transient_data['timestamp']) && (time() - $transient_data['timestamp']) < 1800) {
1033 $booking_data = $transient_data;
1034 // Restore to session
1035 $_SESSION['yatra_booking'] = $booking_data;
1036 $_SESSION['yatra_booking_token'] = $booking_token;
1037 }
1038 }
1039 } catch (Exception $e) {
1040 // Continue without transient data
1041 }
1042 }
1043 }
1044
1045 // Check if session is expired (30 minutes)
1046 if (!empty($booking_data['timestamp'])) {
1047 $session_age = time() - $booking_data['timestamp'];
1048 if ($session_age > 1800) { // 30 minutes
1049 yatra_clear_booking_session();
1050 return $key ? $default : [];
1051 }
1052 }
1053
1054 if ($key === null) {
1055 return $booking_data;
1056 }
1057
1058 return $booking_data[$key] ?? $default;
1059 }
1060
1061 /**
1062 * Clear booking session data (PHP session, booking token, and REST backup transient).
1063 *
1064 * Without removing the token and transient, yatra_get_booking_session() can repopulate
1065 * checkout data from the transient on the next request after a completed booking.
1066 */
1067 function yatra_clear_booking_session(): void
1068 {
1069 yatra_start_session();
1070
1071 $token = $_SESSION['yatra_booking_token'] ?? null;
1072 if (is_string($token) && $token !== '') {
1073 delete_transient($token);
1074 }
1075
1076 unset($_SESSION['yatra_booking'], $_SESSION['yatra_booking_token']);
1077 }
1078
1079 /**
1080 * Check if booking session exists and is valid
1081 *
1082 * @return bool
1083 */
1084 function yatra_has_booking_session(): bool
1085 {
1086 $booking_data = yatra_get_booking_session();
1087 return !empty($booking_data) && !empty($booking_data['trip_id']);
1088 }
1089
1090 /**
1091 * Fire {@see 'yatra_booking_confirmed'} when a booking reaches `confirmed` from a non-confirmed status.
1092 *
1093 * Core always fired `yatra_booking_status_changed`; Pro modules (Trip Consent, Google Calendar) listen
1094 * on this dedicated action. Call this after any code path that sets a booking to `confirmed` without
1095 * going through {@see \Yatra\Services\BookingService::updateStatus()}.
1096 *
1097 * @param int $bookingId Booking ID.
1098 * @param string $previousStatus Booking status in the database immediately before confirming.
1099 */
1100 function yatra_trigger_booking_confirmed(int $bookingId, string $previousStatus): void
1101 {
1102 if ($bookingId < 1 || $previousStatus === 'confirmed') {
1103 return;
1104 }
1105
1106 $repo = new \Yatra\Repositories\BookingRepository();
1107 $booking = $repo->findWithTrip($bookingId);
1108
1109 if (!$booking || ($booking->status ?? '') !== 'confirmed') {
1110 return;
1111 }
1112
1113 /**
1114 * Booking reached confirmed status (was not confirmed before this transition).
1115 *
1116 * @param int $bookingId Booking ID.
1117 * @param object $booking Row from {@see \Yatra\Repositories\BookingRepository::findWithTrip()}.
1118 */
1119 do_action('yatra_booking_confirmed', $bookingId, $booking);
1120 }
1121
1122 /**
1123 * ============================================
1124 * REMAINING PAYMENT SESSION MANAGEMENT
1125 * ============================================
1126 */
1127
1128 /**
1129 * Set remaining payment session data
1130 *
1131 * @param array $data Remaining payment data to store
1132 */
1133 function yatra_set_remaining_session(array $data): void
1134 {
1135 yatra_start_session();
1136
1137 // Clear checkout session fully (including token + transient) before remaining-payment flow
1138 yatra_clear_booking_session();
1139
1140 $_SESSION['yatra_remaining'] = array_merge(
1141 $data,
1142 ['timestamp' => time()]
1143 );
1144
1145 // Ensure session data is written to storage immediately
1146 if (session_status() === PHP_SESSION_ACTIVE) {
1147 session_write_close();
1148 }
1149 }
1150
1151 /**
1152 * Get remaining payment session data
1153 *
1154 * @param string|null $key Specific key to retrieve, or null for all data
1155 * @param mixed $default Default value if key not found
1156 * @return mixed
1157 */
1158 function yatra_get_remaining_session(?string $key = null, $default = null)
1159 {
1160 yatra_start_session();
1161
1162 $remaining_data = $_SESSION['yatra_remaining'] ?? [];
1163
1164 // Check if session is expired (30 minutes)
1165 if (!empty($remaining_data['timestamp'])) {
1166 $session_age = time() - $remaining_data['timestamp'];
1167 if ($session_age > 1800) { // 30 minutes
1168 yatra_clear_remaining_session();
1169 return $key ? $default : [];
1170 }
1171 }
1172
1173 if ($key === null) {
1174 return $remaining_data;
1175 }
1176
1177 return $remaining_data[$key] ?? $default;
1178 }
1179
1180 /**
1181 * Clear remaining payment session data
1182 */
1183 function yatra_clear_remaining_session(): void
1184 {
1185 yatra_start_session();
1186 unset($_SESSION['yatra_remaining']);
1187 }
1188
1189 /**
1190 * Check if remaining payment session exists and is valid
1191 *
1192 * @return bool
1193 */
1194 function yatra_has_remaining_session(): bool
1195 {
1196 $remaining_data = yatra_get_remaining_session();
1197 return !empty($remaining_data) && !empty($remaining_data['booking_id']);
1198 }
1199
1200 /**
1201 * Get the active checkout session type
1202 *
1203 * @return string|null 'remaining' if remaining session exists, 'booking' if booking session exists, null if neither
1204 */
1205 function yatra_get_checkout_session_type(): ?string
1206 {
1207 if (yatra_has_remaining_session()) {
1208 return 'remaining';
1209 }
1210
1211 if (yatra_has_booking_session()) {
1212 return 'booking';
1213 }
1214
1215 return null;
1216 }
1217
1218 /**
1219 * Get the active checkout session data (remaining or booking)
1220 *
1221 * @return array Session data with 'type' key indicating session type
1222 */
1223 function yatra_get_active_checkout_session(): array
1224 {
1225 if (yatra_has_remaining_session()) {
1226 $data = yatra_get_remaining_session();
1227 $data['session_type'] = 'remaining';
1228 return $data;
1229 }
1230
1231 if (yatra_has_booking_session()) {
1232 $data = yatra_get_booking_session();
1233 $data['session_type'] = 'booking';
1234 return $data;
1235 }
1236
1237 return [];
1238 }
1239
1240 /**
1241 * Get booking/checkout URL
1242 *
1243 * Logic:
1244 * 1. If custom booking page is set → return that page's URL
1245 * 2. Otherwise → return dynamic URL using booking_base from settings (e.g., /bookings/)
1246 *
1247 * @return string Booking URL
1248 */
1249 function yatra_get_checkout_url(): string
1250 {
1251 $permalink_structure = get_option('permalink_structure');
1252 $is_plain = empty($permalink_structure);
1253
1254 // Check if custom booking page is set via SettingsService
1255 if (SettingsService::useCustomBookingPage()) {
1256 $page_id = SettingsService::getBookingPageId();
1257 if ($page_id > 0) {
1258 return get_permalink($page_id);
1259 }
1260 }
1261
1262 // Default dynamic URL using booking base from settings
1263 $base = SettingsService::getBookingBase();
1264 if ($is_plain) {
1265 return add_query_arg(['yatra_page' => $base], home_url('/'));
1266 }
1267
1268 return home_url('/' . $base . '/');
1269 }
1270
1271 /**
1272 * Front-end URL for booking confirmation for a given reference.
1273 *
1274 * Booking confirmation is pageless: Yatra serves it via rewrite rules and query vars,
1275 * not a WordPress page permalink. Pretty URLs use /{booking_base}/confirmation/{reference}/.
1276 * Plain permalinks use ?yatra_booking_confirmation={reference}.
1277 *
1278 * To use a real WordPress page as the base (advanced), filter {@see 'yatra_booking_confirmation_base_url'}.
1279 * Legacy /booking-confirmation/{reference}/ remains registered in rewrites for old links.
1280 *
1281 * @param string $reference Booking reference segment (may be empty for base URL only).
1282 * @return string Full URL.
1283 */
1284 function yatra_get_booking_confirmation_url(string $reference = ''): string
1285 {
1286 $reference = (string) $reference;
1287 $permalink_structure = get_option('permalink_structure');
1288 $is_plain = empty($permalink_structure);
1289
1290 if ($is_plain) {
1291 if ($reference === '') {
1292 $url = home_url('/');
1293 } else {
1294 $url = add_query_arg('yatra_booking_confirmation', $reference, home_url('/'));
1295 }
1296 } else {
1297 $booking_base = trim((string) SettingsService::getBookingBase(), '/');
1298 if ($booking_base === '') {
1299 $booking_base = 'book';
1300 }
1301 $virtual_base = home_url('/' . $booking_base . '/confirmation/');
1302
1303 /**
1304 * Override the base URL for booking confirmation (before the reference path segment).
1305 * Return a non-empty string to use a custom base (e.g. get_permalink( $page_id )).
1306 * Default null keeps the pageless virtual URL from Settings → booking base.
1307 *
1308 * @param string|null $base_url Custom base, or null to use virtual URL.
1309 * @param string $reference Booking reference (may be empty).
1310 */
1311 $base_url = apply_filters('yatra_booking_confirmation_base_url', null, $reference);
1312 if (!is_string($base_url) || $base_url === '') {
1313 $base_url = $virtual_base;
1314 }
1315
1316 if ($reference === '') {
1317 $url = trailingslashit($base_url);
1318 } else {
1319 $url = trailingslashit($base_url) . $reference . '/';
1320 }
1321 }
1322
1323 /**
1324 * Filter the booking confirmation URL.
1325 *
1326 * @param string $url Built URL.
1327 * @param string $reference Booking reference (may be empty).
1328 */
1329 return (string) apply_filters('yatra_booking_confirmation_url', $url, $reference);
1330 }
1331
1332 /**
1333 * Front-end URL to verify a customer email (checkout registration / account).
1334 *
1335 * Pretty permalinks: /yatra-verify-email/{token}/ (rewrite + query var).
1336 * Plain permalinks: ?yatra_verify_email={token} on the home URL (same as {@see \Yatra\Core\Routing\PermalinkCanonical}).
1337 *
1338 * @param string $secure_token URL-safe token (base64-derived; only [A-Za-z0-9_-] used in the path/query).
1339 */
1340 function yatra_get_email_verification_url(string $secure_token): string
1341 {
1342 $t = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $secure_token) ?? '';
1343 if ($t === '') {
1344 return home_url('/');
1345 }
1346
1347 $permalink_structure = get_option('permalink_structure');
1348 $is_plain = empty($permalink_structure);
1349
1350 if ($is_plain) {
1351 $url = add_query_arg('yatra_verify_email', $t, home_url('/'));
1352 } else {
1353 $url = trailingslashit(home_url('/yatra-verify-email/' . $t . '/'));
1354 }
1355
1356 /**
1357 * Filter the customer email verification URL.
1358 *
1359 * @param string $url Full verification URL.
1360 * @param string $token Sanitized token segment.
1361 */
1362 return (string) apply_filters('yatra_email_verification_url', $url, $t);
1363 }
1364
1365 /**
1366 * ============================================
1367 * ARCHIVE LISTING (plain permalinks pagination)
1368 * ============================================
1369 */
1370
1371 /**
1372 * Items per page from WordPress Reading settings ("Blog pages show at most").
1373 * Used for Yatra front-end listings (trips, taxonomies, activity/destination/category archives).
1374 *
1375 * @return int At least 1.
1376 */
1377 function yatra_get_posts_per_page(): int
1378 {
1379 $n = absint((int) get_option('posts_per_page', 10));
1380
1381 return (int) apply_filters('yatra_posts_per_page', max(1, $n));
1382 }
1383
1384 /**
1385 * Current page number for Yatra archive templates (activity, destination, trip category).
1386 * Handles plain URLs where WordPress may use {@see 'paged'} or {@see 'page'} on the front page.
1387 */
1388 function yatra_get_archive_listing_paged(): int
1389 {
1390 if (isset($_GET['paged']) && $_GET['paged'] !== '') {
1391 return max(1, absint(wp_unslash($_GET['paged'])));
1392 }
1393
1394 if (!empty($_GET['yatra_page']) && isset($_GET['page']) && $_GET['page'] !== '') {
1395 return max(1, absint(wp_unslash($_GET['page'])));
1396 }
1397
1398 $p = (int) get_query_var('paged');
1399 if ($p > 0) {
1400 return max(1, $p);
1401 }
1402
1403 $p = (int) get_query_var('page');
1404
1405 return max(1, $p);
1406 }
1407
1408 /**
1409 * Result summary for destination / activity / trip-category browse pages (parity with trip grid header).
1410 *
1411 * @param string $items_label Plural noun, e.g. translated "destinations".
1412 */
1413 function yatra_archive_browse_results_line(int $start, int $end, int $total, int $page, int $pages, string $items_label): string
1414 {
1415 if ($total <= 0) {
1416 return '';
1417 }
1418
1419 return sprintf(
1420 /* translators: 1–2: range, 3: total, 4: item type, 5–6: pagination */
1421 __('Showing %1$d–%2$d of %3$d %4$s (page %5$d of %6$d)', 'yatra'),
1422 $start,
1423 $end,
1424 $total,
1425 $items_label,
1426 $page,
1427 $pages
1428 );
1429 }
1430
1431 /**
1432 * Request path (leading slash, no query string) for same-page links. Strips /page/N/ pagination segments.
1433 */
1434 function yatra_get_current_request_path_for_query_urls(): string
1435 {
1436 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
1437 $base_path = strtok($request_uri, '?') ?: '/';
1438 $base_path = rtrim((string) $base_path, '/');
1439 $base_path = preg_replace('#/page/[0-9]+#', '', $base_path);
1440 $base_path = rtrim($base_path, '/');
1441
1442 if ($base_path === '') {
1443 return '/';
1444 }
1445
1446 return $base_path[0] === '/' ? $base_path : '/' . $base_path;
1447 }
1448
1449 /**
1450 * Full URL for the same archive request with a different page (preserves yatra_page and other args).
1451 * Uses the current request path so /destination/, /activity/, /trip-category/ stay on the same listing.
1452 */
1453 function yatra_build_archive_listing_url(int $page_num): string
1454 {
1455 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1456
1457 $qvYatra = (string) get_query_var('yatra_page');
1458 if ($qvYatra !== '' && (!isset($params['yatra_page']) || $params['yatra_page'] === '')) {
1459 $params['yatra_page'] = $qvYatra;
1460 }
1461
1462 if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) {
1463 unset($params['page']);
1464 }
1465
1466 if ($page_num > 1) {
1467 $params['paged'] = (string) $page_num;
1468 } else {
1469 unset($params['paged'], $params['page']);
1470 }
1471
1472 $path = yatra_get_current_request_path_for_query_urls();
1473 $query = http_build_query($params);
1474
1475 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1476 }
1477
1478 /**
1479 * Same request path with a different paged query arg (strips an existing /page/N/ segment first).
1480 * For taxonomy trip lists and other templates not rooted at home_url('/').
1481 */
1482 function yatra_build_current_request_paged_url(int $page_num): string
1483 {
1484 $page_num = max(1, $page_num);
1485 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1486
1487 if ($page_num > 1) {
1488 $params['paged'] = (string) $page_num;
1489 } else {
1490 unset($params['paged'], $params['page']);
1491 }
1492
1493 $path = yatra_get_current_request_path_for_query_urls();
1494 $query = http_build_query($params);
1495
1496 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1497 }
1498
1499 /**
1500 * Same request path with trip sort (TripRepository / TripListingService). Resets pagination.
1501 *
1502 * @param string $sort Allowed: '' (recommended), most_popular, price_low, price_high, rating_high, duration_short, duration_long.
1503 */
1504 function yatra_build_current_request_sort_url(string $sort): string
1505 {
1506 $allowed = ['', 'most_popular', 'price_low', 'price_high', 'rating_high', 'duration_short', 'duration_long'];
1507 if (!in_array($sort, $allowed, true)) {
1508 $sort = '';
1509 }
1510
1511 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1512 unset($params['paged'], $params['page']);
1513 if ($sort !== '') {
1514 $params['sort'] = $sort;
1515 } else {
1516 unset($params['sort']);
1517 }
1518
1519 $path = yatra_get_current_request_path_for_query_urls();
1520 $query = http_build_query($params);
1521
1522 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1523 }
1524
1525 /**
1526 * Compare two archive listing rows (activity, destination, or category) by sort key.
1527 */
1528 function yatra_compare_archive_listing_row_pair(object $a, object $b, string $sort): int
1529 {
1530 $nameA = isset($a->name) ? strtolower((string) $a->name) : '';
1531 $nameB = isset($b->name) ? strtolower((string) $b->name) : '';
1532 $tripsA = isset($a->trips_count) ? (int) $a->trips_count : 0;
1533 $tripsB = isset($b->trips_count) ? (int) $b->trips_count : 0;
1534 $ratingA = isset($a->avg_rating) ? (float) $a->avg_rating : 0.0;
1535 $ratingB = isset($b->avg_rating) ? (float) $b->avg_rating : 0.0;
1536
1537 switch ($sort) {
1538 case 'trips_desc':
1539 return $tripsB <=> $tripsA;
1540 case 'trips_asc':
1541 return $tripsA <=> $tripsB;
1542 case 'name_asc':
1543 return $nameA <=> $nameB;
1544 case 'name_desc':
1545 return $nameB <=> $nameA;
1546 case 'rating_desc':
1547 default:
1548 $cmp = $ratingB <=> $ratingA;
1549 if (0 === $cmp) {
1550 return $tripsB <=> $tripsA;
1551 }
1552
1553 return $cmp;
1554 }
1555 }
1556
1557 /**
1558 * Invokable comparator for {@see yatra_sort_archive_listing_stats_rows()}.
1559 *
1560 * @internal
1561 */
1562 final class Yatra_Archive_Listing_Stats_Comparator
1563 {
1564 /** @var string */
1565 private $sort;
1566
1567 public function __construct(string $sort)
1568 {
1569 $this->sort = $sort;
1570 }
1571
1572 /**
1573 * @param object $a
1574 * @param object $b
1575 */
1576 public function __invoke($a, $b): int
1577 {
1578 return yatra_compare_archive_listing_row_pair($a, $b, $this->sort);
1579 }
1580 }
1581
1582 /**
1583 * Sort archive listing rows in place (stats objects from repository).
1584 */
1585 function yatra_sort_archive_listing_stats_rows(array &$items, string $sort): void
1586 {
1587 if (empty($items)) {
1588 return;
1589 }
1590
1591 usort($items, new Yatra_Archive_Listing_Stats_Comparator($sort));
1592 }
1593
1594 /**
1595 * Sort dropdown URL: same archive, page reset to 1, yatra_sort applied (preserves yatra_page etc.).
1596 */
1597 function yatra_build_archive_listing_sort_url(string $yatra_sort): string
1598 {
1599 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1600 unset($params['paged'], $params['page']);
1601 if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) {
1602 unset($params['page']);
1603 }
1604 $params['yatra_sort'] = $yatra_sort;
1605
1606 $path = yatra_get_current_request_path_for_query_urls();
1607 $query = http_build_query($params);
1608
1609 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1610 }
1611
1612 /**
1613 * ============================================
1614 * PERMALINK HELPERS
1615 * ============================================
1616 */
1617
1618 /**
1619 * Get destination permalink
1620 *
1621 * @param object|int $destination Destination object with slug property, or destination ID
1622 * @return string Destination permalink URL
1623 */
1624 function yatra_get_destination_permalink($destination): string
1625 {
1626 if (is_numeric($destination)) {
1627 global $wpdb;
1628 $table = ClassificationsTable::getTableName();
1629 $destination = $wpdb->get_row($wpdb->prepare(
1630 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1631 (int) $destination,
1632 ClassificationTypes::DESTINATION
1633 ));
1634 }
1635
1636 $slug = is_object($destination) ? ($destination->slug ?? '') : '';
1637
1638 if (empty($slug)) {
1639 return '';
1640 }
1641
1642 $base = SettingsService::getString('destination_base', 'destination');
1643 $permalink_structure = get_option('permalink_structure');
1644 $is_plain = empty($permalink_structure);
1645
1646 if ($is_plain) {
1647 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'destination';
1648
1649 return add_query_arg([$key => $slug], home_url('/'));
1650 }
1651
1652 return home_url('/' . $base . '/' . $slug . '/');
1653 }
1654
1655 /**
1656 * Get activity permalink
1657 *
1658 * @param object|int $activity Activity object with slug property, or activity ID
1659 * @return string Activity permalink URL
1660 */
1661 function yatra_get_activity_permalink($activity): string
1662 {
1663 if (is_numeric($activity)) {
1664 global $wpdb;
1665 $table = ClassificationsTable::getTableName();
1666 $activity = $wpdb->get_row($wpdb->prepare(
1667 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1668 (int) $activity,
1669 ClassificationTypes::ACTIVITY
1670 ));
1671 }
1672
1673 $slug = is_object($activity) ? ($activity->slug ?? '') : '';
1674
1675 if (empty($slug)) {
1676 return '';
1677 }
1678
1679 $base = SettingsService::getString('activity_base', 'activity');
1680 $permalink_structure = get_option('permalink_structure');
1681 $is_plain = empty($permalink_structure);
1682
1683 if ($is_plain) {
1684 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'activity';
1685
1686 return add_query_arg([$key => $slug], home_url('/'));
1687 }
1688
1689 return home_url('/' . $base . '/' . $slug . '/');
1690 }
1691
1692 /**
1693 * Get trip category permalink
1694 *
1695 * @param object|int $category Category object with slug property, or category ID
1696 * @return string Category permalink URL
1697 */
1698 function yatra_get_category_permalink($category): string
1699 {
1700 if (is_numeric($category)) {
1701 global $wpdb;
1702 $table = ClassificationsTable::getTableName();
1703 $category = $wpdb->get_row($wpdb->prepare(
1704 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1705 (int) $category,
1706 ClassificationTypes::CATEGORY
1707 ));
1708 }
1709
1710 $slug = is_object($category) ? ($category->slug ?? '') : '';
1711
1712 if (empty($slug)) {
1713 return '';
1714 }
1715
1716 $base = SettingsService::getString('trip_category_base', 'trip-category');
1717 $permalink_structure = get_option('permalink_structure');
1718 $is_plain = empty($permalink_structure);
1719
1720 if ($is_plain) {
1721 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip-category';
1722
1723 return add_query_arg([$key => $slug], home_url('/'));
1724 }
1725
1726 return home_url('/' . $base . '/' . $slug . '/');
1727 }
1728
1729 /**
1730 * Get trip permalink
1731 *
1732 * @param object|int $trip Trip object with slug property, or trip ID
1733 * @return string Trip permalink URL
1734 */
1735 function yatra_get_trip_permalink($trip): string
1736 {
1737 if (is_numeric($trip)) {
1738 global $wpdb;
1739 $table = TripsTable::getTableName();
1740 $trip = $wpdb->get_row($wpdb->prepare(
1741 "SELECT slug FROM {$table} WHERE id = %d",
1742 (int) $trip
1743 ));
1744 }
1745
1746 $slug = is_object($trip) ? ($trip->slug ?? '') : '';
1747
1748 if (empty($slug)) {
1749 return '';
1750 }
1751
1752 $base = SettingsService::getTripBase();
1753 $permalink_structure = get_option('permalink_structure');
1754 $is_plain = empty($permalink_structure);
1755
1756 if ($is_plain) {
1757 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip';
1758
1759 return add_query_arg([$key => $slug], home_url('/'));
1760 }
1761
1762 return home_url('/' . $base . '/' . $slug . '/');
1763 }
1764
1765 /**
1766 * Canonical URL for the trip archive / filter listing (respects Settings trip base).
1767 * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/.
1768 */
1769 function yatra_get_trip_listing_url(): string
1770 {
1771 $base = SettingsService::getTripBase();
1772 $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'trip';
1773 $permalink_structure = (string) get_option('permalink_structure', '');
1774
1775 if ($permalink_structure === '') {
1776 $url = esc_url(add_query_arg('yatra_page', $base, home_url('/')));
1777 } else {
1778 $url = trailingslashit(home_url('/' . $base . '/'));
1779 }
1780
1781 return (string) apply_filters('yatra_trip_listing_url', $url, $base);
1782 }
1783
1784 /**
1785 * Canonical URL for browse-all taxonomy listings (destinations, activities, trip categories).
1786 * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/.
1787 *
1788 * @param string $listing_type One of: destination, activity, category
1789 */
1790 function yatra_get_taxonomy_listing_url(string $listing_type): string
1791 {
1792 $map = [
1793 'destination' => SettingsService::getString('destination_base', 'destination'),
1794 'activity' => SettingsService::getString('activity_base', 'activity'),
1795 'category' => SettingsService::getString('trip_category_base', 'trip-category'),
1796 ];
1797 $base = $map[$listing_type] ?? '';
1798 $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'destination';
1799 $permalink_structure = (string) get_option('permalink_structure', '');
1800
1801 if ($permalink_structure === '') {
1802 $url = esc_url(add_query_arg('yatra_page', $base, home_url('/')));
1803 } else {
1804 $url = trailingslashit(home_url('/' . $base . '/'));
1805 }
1806
1807 return (string) apply_filters('yatra_taxonomy_listing_url', $url, $listing_type, $base);
1808 }
1809
1810 /**
1811 * Decode trips.price_types for listing-card logic (DB may store JSON string or array).
1812 *
1813 * @return array<int, array<string, mixed>>
1814 */
1815 function yatra_trip_listing_decode_price_types(object $trip): array
1816 {
1817 $pts = $trip->price_types ?? null;
1818 if (is_string($pts) && $pts !== '') {
1819 $decoded = json_decode($pts, true);
1820 $pts = is_array($decoded) ? $decoded : [];
1821 } elseif (!is_array($pts)) {
1822 $pts = [];
1823 }
1824 if ($pts === [] && method_exists($trip, 'getPriceTypes')) {
1825 $got = $trip->getPriceTypes();
1826 $pts = is_array($got) ? $got : [];
1827 }
1828
1829 return $pts;
1830 }
1831
1832 /**
1833 * Lowercase keys for traveler tier labels (used to strip mis-tagged classifications).
1834 *
1835 * @return array<string, true>
1836 */
1837 function yatra_trip_listing_traveler_tier_label_keys(object $trip): array
1838 {
1839 if (($trip->pricing_type ?? '') !== 'traveler_based') {
1840 return [];
1841 }
1842 $keys = [];
1843 foreach (yatra_trip_listing_decode_price_types($trip) as $pt) {
1844 if (!is_array($pt)) {
1845 continue;
1846 }
1847 foreach (['label', 'category_label', 'title'] as $k) {
1848 if (!empty($pt[$k]) && is_string($pt[$k])) {
1849 $t = strtolower(trim($pt[$k]));
1850 if ($t !== '') {
1851 $keys[$t] = true;
1852 }
1853 break;
1854 }
1855 }
1856 }
1857
1858 return $keys;
1859 }
1860
1861 /**
1862 * Ordered unique labels for the listing card “Traveler types” row.
1863 *
1864 * @return list<string>
1865 */
1866 function yatra_trip_listing_traveler_type_labels_for_card(object $trip): array
1867 {
1868 if (($trip->pricing_type ?? '') !== 'traveler_based') {
1869 return [];
1870 }
1871 $labels = [];
1872 $seen = [];
1873 foreach (yatra_trip_listing_decode_price_types($trip) as $pt) {
1874 if (!is_array($pt)) {
1875 continue;
1876 }
1877 foreach (['label', 'category_label', 'title'] as $k) {
1878 if (!empty($pt[$k]) && is_string($pt[$k])) {
1879 $lab = trim($pt[$k]);
1880 if ($lab === '') {
1881 break;
1882 }
1883 $lk = strtolower($lab);
1884 if (!isset($seen[$lk])) {
1885 $seen[$lk] = true;
1886 $labels[] = $lab;
1887 }
1888 break;
1889 }
1890 }
1891 }
1892
1893 return $labels;
1894 }
1895
1896 /**
1897 * Format start → end for listing cards; avoids repeating the same country when both
1898 * strings are "City, Country".
1899 */
1900 function yatra_format_trip_listing_route_line(string $start, string $end): string
1901 {
1902 $start = trim($start);
1903 $end = trim($end);
1904 if ($start === '') {
1905 return $end;
1906 }
1907 if ($end === '') {
1908 return $start;
1909 }
1910 if (strcasecmp($start, $end) === 0) {
1911 return $start;
1912 }
1913 if (strpos($start, ',') !== false && strpos($end, ',') !== false) {
1914 $s_parts = array_map('trim', explode(',', $start, 2));
1915 $e_parts = array_map('trim', explode(',', $end, 2));
1916 if (count($s_parts) === 2 && count($e_parts) === 2
1917 && strcasecmp($s_parts[1], $e_parts[1]) === 0) {
1918 return $s_parts[0] . '' . $e_parts[0] . ', ' . $s_parts[1];
1919 }
1920 }
1921
1922 return $start . '' . $end;
1923 }
1924
1925 /**
1926 * Human label for trip_type column (listing card meta).
1927 */
1928 function yatra_trip_listing_trip_type_label(?string $trip_type): string
1929 {
1930 $t = (string) $trip_type;
1931 $map = [
1932 'single_day' => __('Single day', 'yatra'),
1933 'multi_day' => __('Multi-day', 'yatra'),
1934 'flexible' => __('Flexible', 'yatra'),
1935 ];
1936
1937 return $map[$t] ?? '';
1938 }
1939
1940 /**
1941 * Rating block for listing cards: prefers SQL aggregates (average_rating, review_count)
1942 * when the hydrated reviews array is empty.
1943 *
1944 * @param array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string} $from_reviews
1945 * @return array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string}
1946 */
1947 function yatra_trip_listing_card_rating_data(object $trip, array $from_reviews): array
1948 {
1949 $has = !empty($from_reviews['has_rating']);
1950 $avg = (float) ($from_reviews['average_rating'] ?? 0);
1951 $cnt = (int) ($from_reviews['review_count'] ?? 0);
1952 $fmt = (string) ($from_reviews['formatted_rating'] ?? '0.0');
1953
1954 if ($cnt === 0 || !$has || $avg <= 0) {
1955 $q_avg = isset($trip->average_rating) ? (float) $trip->average_rating : null;
1956 $q_cnt = isset($trip->review_count) ? (int) $trip->review_count : null;
1957 if (($q_cnt === null || $q_cnt === 0) && isset($trip->reviews_count)) {
1958 $q_cnt = (int) $trip->reviews_count;
1959 }
1960 if ($q_cnt !== null && $q_cnt > 0 && $q_avg !== null && $q_avg > 0) {
1961 $avg = round($q_avg, 1);
1962 $cnt = $q_cnt;
1963 $fmt = number_format($avg, 1);
1964 $has = true;
1965 }
1966 }
1967
1968 return [
1969 'has_rating' => $has && $avg > 0 && $cnt > 0,
1970 'average_rating' => $avg,
1971 'review_count' => $cnt,
1972 'formatted_rating' => $fmt,
1973 ];
1974 }
1975
1976 /**
1977 * Avoid repeating the same classification label in the destination, activity, and category
1978 * rows on listing cards (traveler tier labels wrongly linked as classifications, or same
1979 * term attached in multiple roles).
1980 *
1981 * @param array<int, object> $destinations
1982 * @param array<int, object> $activities
1983 * @param array<int, object> $categories
1984 * @return array{0: array<int, object>, 1: array<int, object>, 2: array<int, object>}
1985 */
1986 function yatra_trip_listing_filter_classification_duplicates(array $destinations, array $activities, array $categories, object $trip): array
1987 {
1988 $tier_keys = yatra_trip_listing_traveler_tier_label_keys($trip);
1989
1990 $strip_tiers = static function (array $items) use ($tier_keys): array {
1991 if ($tier_keys === []) {
1992 return $items;
1993 }
1994
1995 return array_values(array_filter($items, static function ($item) use ($tier_keys) {
1996 $n = strtolower(trim((string) ($item->name ?? '')));
1997
1998 return $n === '' || !isset($tier_keys[$n]);
1999 }));
2000 };
2001
2002 $destinations = $strip_tiers($destinations);
2003 $activities = $strip_tiers($activities);
2004 $categories = $strip_tiers($categories);
2005
2006 $seen = [];
2007 $dedupe = static function (array $items) use (&$seen): array {
2008 $out = [];
2009 foreach ($items as $item) {
2010 $n = strtolower(trim((string) ($item->name ?? '')));
2011 if ($n === '') {
2012 $out[] = $item;
2013 continue;
2014 }
2015 if (isset($seen[$n])) {
2016 continue;
2017 }
2018 $seen[$n] = true;
2019 $out[] = $item;
2020 }
2021
2022 return $out;
2023 };
2024
2025 $destinations = $dedupe($destinations);
2026 $activities = $dedupe($activities);
2027 $categories = $dedupe($categories);
2028
2029 return [$destinations, $activities, $categories];
2030 }
2031
2032 /**
2033 * Check if we're on a trip listing page
2034 *
2035 * @return bool True if on a trip listing page
2036 */
2037 function yatra_is_trip_listing(): bool
2038 {
2039 global $yatra_trip_list;
2040
2041 // Check for trip list context (base trip listing page)
2042 if (!empty($yatra_trip_list)) {
2043 return true;
2044 }
2045
2046 // Check if we're on the main trips listing page
2047 $trip_base = SettingsService::getTripBase();
2048 $request_uri = $_SERVER['REQUEST_URI'] ?? '';
2049 $parsed_url = parse_url($request_uri, PHP_URL_PATH);
2050
2051 if ($parsed_url && strpos($parsed_url, '/' . $trip_base) === 0) {
2052 $path_parts = array_values(array_filter(explode('/', trim($parsed_url, '/'))));
2053 if ($path_parts === [] || ($path_parts[0] ?? '') !== $trip_base) {
2054 return false;
2055 }
2056 // /trip/ or /trip/page/2/ (WordPress paged archives)
2057 if (count($path_parts) === 1) {
2058 return true;
2059 }
2060 if (count($path_parts) === 3 && ($path_parts[1] ?? '') === 'page' && ctype_digit((string) ($path_parts[2] ?? ''))) {
2061 return true;
2062 }
2063 }
2064
2065 return false;
2066 }
2067
2068 /**
2069 * Check if we're on a taxonomy page (destination, activity, category)
2070 *
2071 * @return bool True if on a taxonomy page
2072 */
2073 function yatra_is_taxonomy_page(): bool
2074 {
2075 global $yatra_taxonomy_data;
2076 return !empty($yatra_taxonomy_data);
2077 }
2078
2079 /**
2080 * Check if we're on an activity listing page
2081 *
2082 * @return bool True if on an activity listing page
2083 */
2084 function yatra_is_activity_listing(): bool
2085 {
2086 return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'activities';
2087 }
2088
2089 /**
2090 * Check if we're on a destination listing page
2091 *
2092 * @return bool True if on a destination listing page
2093 */
2094 function yatra_is_destination_listing(): bool
2095 {
2096 return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'destinations';
2097 }
2098
2099 /**
2100 * Check if we're on an account page
2101 *
2102 * @return bool True if on an account page
2103 */
2104 function yatra_is_account_page(): bool
2105 {
2106 if (!empty($GLOBALS['yatra_loading_react_account_page'])) {
2107 return true;
2108 }
2109
2110 if ((string) get_query_var('yatra_account_page') !== '') {
2111 return true;
2112 }
2113
2114 global $post;
2115 if ($post && function_exists('has_shortcode') && isset($post->post_content)
2116 && has_shortcode((string) $post->post_content, 'yatra_my_account')) {
2117 return true;
2118 }
2119
2120 if (!$post) {
2121 return false;
2122 }
2123
2124 $accountPageId = get_option('yatra_my_account_page');
2125 return $accountPageId && (int) $post->ID === (int) $accountPageId;
2126 }
2127
2128 /**
2129 * Get difficulty level permalink
2130 *
2131 * @param object|int $difficulty Difficulty object with slug property, or difficulty ID
2132 * @return string Difficulty permalink URL
2133 */
2134 function yatra_get_difficulty_permalink($difficulty): string
2135 {
2136 if (is_numeric($difficulty)) {
2137 global $wpdb;
2138 $table = ClassificationsTable::getTableName();
2139 $difficulty = $wpdb->get_row($wpdb->prepare(
2140 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
2141 (int) $difficulty,
2142 ClassificationTypes::DIFFICULTY
2143 ));
2144 }
2145
2146 $slug = is_object($difficulty) ? ($difficulty->slug ?? '') : '';
2147
2148 if (empty($slug)) {
2149 return '';
2150 }
2151
2152 $base = SettingsService::getString('difficulty_base', 'difficulty');
2153
2154 return home_url('/' . $base . '/' . $slug . '/');
2155 }
2156
2157 /**
2158 * Load a template file with theme override support
2159 *
2160 * This function allows themes to override plugin templates by placing them in:
2161 * theme/yatra/template-name.php
2162 *
2163 * If no theme override exists, loads from plugin templates directory.
2164 *
2165 * @param string $template_name Template file name (without .php extension)
2166 * @param array $args Arguments to extract and make available in template
2167 * @param string $template_path Template path within plugin (default: 'templates/')
2168 * @param array $data Alternative data array (won't be extracted, available as $data)
2169 * @return void
2170 */
2171 function yatra_get_template(string $template_name, array $args = [], string $template_path = 'templates/', array $data = []): void
2172 {
2173 $template_name = ltrim($template_name, '/');
2174
2175 // Check if theme has override
2176 $theme_template = locate_template([
2177 'yatra/' . $template_name . '.php',
2178 'yatra/' . $template_name
2179 ]);
2180
2181 if ($theme_template) {
2182 // Load from theme
2183 $template_file = $theme_template;
2184 } else {
2185 // Load from plugin
2186 $template_file = YATRA_PLUGIN_PATH . ltrim($template_path, '/') . '/' . $template_name . '.php';
2187 }
2188
2189 // Extract arguments to make them available as individual variables
2190 if (!empty($args)) {
2191 extract($args);
2192 }
2193
2194 // Make data available as $data array (not extracted)
2195 if (!empty($data)) {
2196 $data = $data;
2197 }
2198
2199 // Include the template
2200 if (file_exists($template_file)) {
2201 include $template_file;
2202 }
2203 }
2204
2205 /**
2206 * Enqueue single trip scripts and styles
2207 *
2208 * @return void
2209 */
2210 function yatra_enqueue_single_trip_scripts(): void
2211 {
2212 // Only enqueue on single trip pages
2213 if (!is_single() || get_post_type() !== 'trip') {
2214 return;
2215 }
2216
2217 // Enqueue the single trip JavaScript
2218 wp_enqueue_script(
2219 'yatra-single-trip',
2220 YATRA_PLUGIN_URL . 'assets/js/single-trip.js',
2221 ['jquery', 'yatra-trip'],
2222 YATRA_VERSION,
2223 true
2224 );
2225
2226 // Localize script data
2227 global $trip;
2228 if ($trip) {
2229 wp_localize_script(
2230 'yatra-single-trip',
2231 'yatraSingleTripData',
2232 [
2233 'tripId' => (int) $trip->id,
2234 'basePrice' => (float) ($trip->base_price ?? 0),
2235 'currencySymbol' => yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency()),
2236 'apiUrls' => [
2237 'groupDiscounts' => rest_url('yatra/v1/discounts/group-discounts')
2238 ]
2239 ]
2240 );
2241 }
2242 }
2243
2244 /**
2245 * Calculate base price for single trip display using CalculationService
2246 *
2247 * @param object $trip Trip object
2248 * @return array Pricing data including base_price, has_availability, has_traveler_pricing, pricing_type
2249 */
2250 function yatra_single_trip_calculate_base_price($trip) {
2251 // Check if availability dates exist (PRIORITY)
2252 $has_availability = !empty($trip->availability_dates) && is_array($trip->availability_dates) && count($trip->availability_dates) > 0;
2253
2254 // Determine pricing type from trip settings
2255 $pricing_type = $trip->pricing_type ?? 'regular';
2256 $has_traveler_pricing = ($pricing_type === 'traveler_based' && !empty($trip->price_types));
2257
2258 // Use CalculationService for consistent pricing
2259 $calculationService = new \Yatra\Services\CalculationService();
2260
2261 // Determine base price using CalculationService logic
2262 $trip_price = 0;
2263
2264 if ($has_availability) {
2265 // Page-load pricing priority (traveler-based):
2266 // - If a default category is marked at trip-level, use that as the base price.
2267 // - Otherwise fall back to lowest price across availability (legacy behavior).
2268 $default_trip_price = 0.0;
2269 if ($has_traveler_pricing && !empty($trip->price_types) && is_array($trip->price_types)) {
2270 $default_price_type = null;
2271 foreach ($trip->price_types as $pt) {
2272 if (is_array($pt)) {
2273 $pt = (object) $pt;
2274 }
2275 if (!empty($pt->is_default)) {
2276 $default_price_type = $pt;
2277 break;
2278 }
2279 }
2280 if ($default_price_type) {
2281 $default_trip_price = (float) ($default_price_type->effective_price
2282 ?? $default_price_type->discounted_price
2283 ?? $default_price_type->original_price
2284 ?? 0);
2285 }
2286 }
2287
2288 if ($default_trip_price > 0) {
2289 $trip_price = $default_trip_price;
2290 } else {
2291 // Get the lowest price from availability dates
2292 $min_price = PHP_FLOAT_MAX;
2293 foreach ($trip->availability_dates as $avail) {
2294 $avail_price = $avail->effective_price ?? $avail->original_price ?? 0;
2295 if ($avail_price > 0 && $avail_price < $min_price) {
2296 $min_price = $avail_price;
2297 }
2298
2299 // Also check price_types within availability if traveler-based
2300 if (!empty($avail->price_types) && is_array($avail->price_types)) {
2301 foreach ($avail->price_types as $pt) {
2302 $pt = (object)$pt;
2303 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
2304 if ($pt_price > 0 && $pt_price < $min_price) {
2305 $min_price = $pt_price;
2306 }
2307 }
2308 }
2309 }
2310
2311 // If no price found from availability, check traveler-based pricing
2312 if ($min_price >= PHP_FLOAT_MAX && $has_traveler_pricing) {
2313 foreach ($trip->price_types as $pt) {
2314 $pt = is_array($pt) ? (object) $pt : $pt;
2315 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
2316 if ($pt_price > 0 && $pt_price < $min_price) {
2317 $min_price = $pt_price;
2318 }
2319 }
2320 }
2321
2322 $trip_price = ($min_price < PHP_FLOAT_MAX) ? $min_price : ($trip->sale_price ?: $trip->original_price);
2323 }
2324 } elseif ($has_traveler_pricing) {
2325 // Get default or first traveler category price
2326 $default_price_type = null;
2327 foreach ($trip->price_types as $pt) {
2328 if (!empty($pt->is_default)) {
2329 $default_price_type = $pt;
2330 break;
2331 }
2332 }
2333 if (!$default_price_type && !empty($trip->price_types)) {
2334 $default_price_type = $trip->price_types[0];
2335 }
2336
2337 // Get the price from the price type - check multiple possible fields
2338 if ($default_price_type) {
2339 $trip_price = 0;
2340 // Try effective_price first, then discounted_price, then original_price
2341 if (!empty($default_price_type->effective_price) && $default_price_type->effective_price > 0) {
2342 $trip_price = (float)$default_price_type->effective_price;
2343 } elseif (!empty($default_price_type->discounted_price) && $default_price_type->discounted_price > 0) {
2344 $trip_price = (float)$default_price_type->discounted_price;
2345 } elseif (!empty($default_price_type->original_price) && $default_price_type->original_price > 0) {
2346 $trip_price = (float)$default_price_type->original_price;
2347 } elseif (!empty($default_price_type->sale_price) && $default_price_type->sale_price > 0) {
2348 $trip_price = (float)$default_price_type->sale_price;
2349 }
2350
2351 // If still no price, try to get the minimum from all price types
2352 if ($trip_price <= 0) {
2353 foreach ($trip->price_types as $pt) {
2354 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
2355 if ($pt_price > 0 && ($trip_price <= 0 || $pt_price < $trip_price)) {
2356 $trip_price = $pt_price;
2357 }
2358 }
2359 }
2360 } else {
2361 $trip_price = $trip->sale_price ?: $trip->original_price;
2362 }
2363 } else {
2364 // Regular pricing
2365 $trip_price = $trip->sale_price > 0 ? $trip->sale_price : $trip->original_price;
2366 }
2367
2368 // Apply CalculationService filter for dynamic pricing (pro plugins)
2369 $base_price = apply_filters('yatra_calculate_base_amount', $trip_price, [
2370 'trip_price' => $trip_price,
2371 'travelers_count' => 1,
2372 'traveler_counts' => ['default' => 1],
2373 'pricing_type' => $pricing_type,
2374 'price_types' => $trip->price_types ?? [],
2375 'trip_id' => $trip->id ?? 0
2376 ]);
2377
2378 return [
2379 'base_price' => $base_price,
2380 'has_availability' => $has_availability,
2381 'has_traveler_pricing' => $has_traveler_pricing,
2382 'pricing_type' => $pricing_type
2383 ];
2384 }
2385
2386 /**
2387 * Get group discounts data for single trip
2388 *
2389 * @param int $trip_id Trip ID
2390 * @return array Group discounts data including has_group_discounts and group_discounts_data
2391 */
2392 function yatra_single_trip_get_group_discounts($trip_id) {
2393 $has_group_discounts = false;
2394 $group_discounts_data = [];
2395 $trip_id = (int) $trip_id;
2396
2397 if ($trip_id <= 0) {
2398 return [
2399 'has_group_discounts' => false,
2400 'group_discounts_data' => [],
2401 ];
2402 }
2403
2404 try {
2405 // Direct controller path avoids rest_do_request / loopback issues on single-trip templates.
2406 if (class_exists(\Yatra\Controllers\DiscountController::class)) {
2407 $ctrl = new \Yatra\Controllers\DiscountController();
2408 $payload = $ctrl->getPublicGroupDiscountDiscoverabilityForTrip($trip_id);
2409 $discounts = isset($payload['discounts']) && is_array($payload['discounts']) ? $payload['discounts'] : [];
2410 if (!empty($payload['has_group_discounts']) && $discounts !== []) {
2411 return [
2412 'has_group_discounts' => true,
2413 'group_discounts_data' => $discounts,
2414 ];
2415 }
2416 }
2417
2418 $row = null;
2419
2420 // Fallback: internal REST then HTTP (e.g. if controller unavailable).
2421 if (class_exists('\WP_REST_Request') && function_exists('rest_do_request')) {
2422 $request = new \WP_REST_Request('GET', '/yatra/v1/discounts/group-discounts');
2423 $request->set_param('trip_ids', [$trip_id]);
2424 $rest_response = rest_do_request($request);
2425 if ($rest_response instanceof \WP_REST_Response && $rest_response->get_status() === 200) {
2426 $row = yatra_single_trip_parse_group_discounts_payload($rest_response->get_data(), $trip_id);
2427 }
2428 }
2429
2430 if (!is_array($row)) {
2431 $api_url = add_query_arg(
2432 ['trip_ids' => [$trip_id]],
2433 rest_url('yatra/v1/discounts/group-discounts')
2434 );
2435 $response = wp_remote_get($api_url, [
2436 'timeout' => 6,
2437 'headers' => [
2438 'Accept' => 'application/json',
2439 ],
2440 ]);
2441
2442 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2443 $data = json_decode(wp_remote_retrieve_body($response), true);
2444 $row = yatra_single_trip_parse_group_discounts_payload($data, $trip_id);
2445 }
2446 }
2447
2448 if (is_array($row) && !empty($row['has_group_discounts']) && !empty($row['discounts']) && is_array($row['discounts'])) {
2449 $has_group_discounts = true;
2450 $group_discounts_data = $row['discounts'];
2451 }
2452 } catch (Exception $e) {
2453 $has_group_discounts = false;
2454 }
2455
2456 return [
2457 'has_group_discounts' => $has_group_discounts,
2458 'group_discounts_data' => $group_discounts_data,
2459 ];
2460 }
2461
2462 /**
2463 * Extract the per-trip object from a group-discounts REST payload (handles optional wrappers).
2464 *
2465 * @param mixed $data
2466 * @return array<string, mixed>|null
2467 */
2468 function yatra_single_trip_parse_group_discounts_payload($data, int $trip_id): ?array {
2469 if (!is_array($data)) {
2470 return null;
2471 }
2472 if (isset($data['data']) && is_array($data['data'])) {
2473 $data = $data['data'];
2474 }
2475 $keyStr = (string) $trip_id;
2476 $row = $data[$trip_id] ?? $data[$keyStr] ?? null;
2477
2478 return is_array($row) ? $row : null;
2479 }
2480
2481 /**
2482 * Payload for single-trip booking UI JS (sidebar date/traveler pricing + group tiers).
2483 * Kept in yatraTripData instead of large HTML data-* attributes on .yatra-booking-card.
2484 *
2485 * @param object $trip Trip model
2486 * @return array{pricingType: string, sidebarAvailability: array<int, array<string, mixed>>, sidebarGroupDiscounts: array<int, array<string, mixed>>}
2487 */
2488 function yatra_single_trip_get_client_booking_payload($trip): array {
2489 $empty = [
2490 'pricingType' => 'regular',
2491 'sidebarAvailability' => [],
2492 'sidebarGroupDiscounts' => [],
2493 ];
2494
2495 if (!is_object($trip) || empty($trip->id)) {
2496 return $empty;
2497 }
2498
2499 $pricing_data = function_exists('yatra_single_trip_calculate_base_price')
2500 ? yatra_single_trip_calculate_base_price($trip)
2501 : ['has_availability' => false, 'pricing_type' => $trip->pricing_type ?? 'regular'];
2502
2503 $pricing_type = (string) ($pricing_data['pricing_type'] ?? ($trip->pricing_type ?? 'regular'));
2504 $has_availability = !empty($pricing_data['has_availability']);
2505
2506 $availability = [];
2507 if ($has_availability && method_exists($trip, 'getAvailabilityDates')) {
2508 foreach ($trip->getAvailabilityDates() as $avail) {
2509 if (!is_object($avail)) {
2510 continue;
2511 }
2512 $price_types_raw = !empty($avail->price_types) && is_array($avail->price_types) ? $avail->price_types : [];
2513 $price_types = [];
2514 foreach ($price_types_raw as $pt) {
2515 if (is_object($pt)) {
2516 $decoded = json_decode(wp_json_encode($pt), true);
2517 $price_types[] = is_array($decoded) ? $decoded : [];
2518 } elseif (is_array($pt)) {
2519 $price_types[] = $pt;
2520 }
2521 }
2522
2523 $availability[] = [
2524 'id' => (int) ($avail->id ?? 0),
2525 'date' => $avail->departure_date ?? '',
2526 'departure_date' => $avail->departure_date ?? '',
2527 'return_date' => (isset($avail->return_date) && $avail->return_date !== '')
2528 ? $avail->return_date
2529 : (isset($avail->arrival_date) ? $avail->arrival_date : null),
2530 'price' => $avail->effective_price ?? $avail->original_price ?? 0,
2531 'original_price' => $avail->original_price ?? 0,
2532 'discounted_price' => $avail->discounted_price ?? null,
2533 'seats_available' => $avail->seats_available ?? 0,
2534 'seats_total' => $avail->seats_total ?? 0,
2535 'status' => $avail->status ?? '',
2536 'is_limited' => (bool) ($avail->is_limited ?? false),
2537 'is_sold_out' => (bool) ($avail->is_sold_out ?? false),
2538 'pricing_type' => $price_types !== [] ? 'traveler_based' : $pricing_type,
2539 'price_types' => $price_types,
2540 ];
2541 }
2542 }
2543
2544 $sidebar_group_discounts = [];
2545 if (function_exists('yatra_single_trip_get_group_discounts')) {
2546 $gd = yatra_single_trip_get_group_discounts((int) $trip->id);
2547 $cards = isset($gd['group_discounts_data']) && is_array($gd['group_discounts_data'])
2548 ? $gd['group_discounts_data']
2549 : [];
2550 $sidebar_group_discounts = apply_filters('yatra_advanced_discount_enabled', false) ? $cards : [];
2551 $sidebar_group_discounts = array_values(array_map(static function ($row) {
2552 if (is_object($row)) {
2553 $decoded = json_decode(wp_json_encode($row), true);
2554
2555 return is_array($decoded) ? $decoded : [];
2556 }
2557
2558 return $row;
2559 }, $sidebar_group_discounts));
2560 }
2561
2562 return [
2563 'pricingType' => $pricing_type,
2564 'sidebarAvailability' => $availability,
2565 'sidebarGroupDiscounts' => $sidebar_group_discounts,
2566 ];
2567 }
2568
2569 // Hook into WordPress enqueue system
2570 add_action('wp_enqueue_scripts', 'yatra_enqueue_single_trip_scripts');
2571
2572 // Yatra page type detection functions
2573 if (!function_exists('yatra_is_trip_page')) {
2574 function yatra_is_trip_page() {
2575 global $trip;
2576 return isset($trip) && !empty($trip);
2577 }
2578 }
2579
2580 if (!function_exists('yatra_is_destination_page')) {
2581 function yatra_is_destination_page() {
2582 global $destination, $yatra_taxonomy_data;
2583
2584 // Check direct global first
2585 if (isset($destination) && !empty($destination)) {
2586 return true;
2587 }
2588
2589 // Check taxonomy data
2590 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'destination') {
2591 return true;
2592 }
2593
2594 return false;
2595 }
2596 }
2597
2598 if (!function_exists('yatra_is_activity_page')) {
2599 function yatra_is_activity_page() {
2600 global $activity, $yatra_taxonomy_data;
2601
2602 // Check direct global first
2603 if (isset($activity) && !empty($activity)) {
2604 return true;
2605 }
2606
2607 // Check taxonomy data
2608 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'activity') {
2609 return true;
2610 }
2611
2612 return false;
2613 }
2614 }
2615
2616 if (!function_exists('yatra_is_category_page')) {
2617 function yatra_is_category_page() {
2618 global $category, $yatra_taxonomy_data;
2619
2620 // Check direct global first
2621 if (isset($category) && !empty($category)) {
2622 return true;
2623 }
2624
2625 // Check taxonomy data
2626 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'category') {
2627 return true;
2628 }
2629
2630 return false;
2631 }
2632 }
2633
2634 if (!function_exists('yatra_is_trip_archive_page')) {
2635 function yatra_is_trip_archive_page() {
2636 $current_url = $_SERVER['REQUEST_URI'] ?? '';
2637 $current_path = parse_url($current_url, PHP_URL_PATH) ?? '';
2638 $trip_base = \Yatra\Services\SettingsService::getTripBase();
2639
2640 // Check for both /trip/ and /trip patterns
2641 $pattern1 = '/' . $trip_base . '/';
2642 $pattern2 = '/' . $trip_base;
2643
2644 return (strpos($current_path, $pattern1) !== false || $current_path === $pattern2) && !yatra_is_trip_page();
2645 }
2646 }
2647
2648 // Yatra only has trip archive pages - no destination/activity/category archive pages
2649
2650 if (!function_exists('yatra_is_listing_page')) {
2651 function yatra_is_listing_page() {
2652 $current_url = $_SERVER['REQUEST_URI'] ?? '';
2653 $current_path = parse_url($current_url, PHP_URL_PATH) ?? '';
2654 return strpos($current_path, '/listing-') !== false;
2655 }
2656 }
2657
2658 if (!function_exists('yatra_is_yatra_page')) {
2659 function yatra_is_yatra_page() {
2660 return yatra_is_trip_page() ||
2661 yatra_is_destination_page() ||
2662 yatra_is_activity_page() ||
2663 yatra_is_category_page() ||
2664 yatra_is_trip_archive_page() ||
2665 yatra_is_listing_page();
2666 }
2667 }
2668
2669 if ( ! function_exists( 'yatra_get_header' ) ) {
2670
2671 function yatra_get_header( $header_name = null ) {
2672 global $wp_version;
2673 if (
2674 version_compare( $wp_version, '5.9', '>=' ) &&
2675 function_exists( 'wp_is_block_theme' ) &&
2676 wp_is_block_theme()
2677 ) {
2678 /*
2679 * Full-site editing themes often omit add_theme_support( 'title-tag' ); the document title is
2680 * injected via template canvas using _block_template_render_title_tag (unconditional). Yatra
2681 * renders this minimal head instead of canvas, so _wp_render_title_tag would no-op and the
2682 * page would have no <title>. Mirror canvas: print title here and drop duplicate core hooks.
2683 */
2684 remove_action( 'wp_head', '_wp_render_title_tag', 1 );
2685 remove_action( 'wp_head', '_block_template_render_title_tag', 1 );
2686 ?>
2687 <!doctype html>
2688 <html <?php language_attributes(); ?>>
2689 <head>
2690 <meta charset="<?php bloginfo( 'charset' ); ?>">
2691 <title><?php echo esc_html( wp_get_document_title() ); ?></title>
2692 <?php wp_head(); ?>
2693 </head>
2694
2695 <body <?php body_class(); ?>>
2696 <?php wp_body_open(); ?>
2697 <div class="wp-site-blocks">
2698 <header class="wp-block-template-part site-header">
2699 <?php block_header_area(); ?>
2700 </header>
2701 <?php
2702 } else {
2703 get_header( $header_name );
2704 }
2705 }
2706 }
2707
2708 if ( ! function_exists( 'yatra_block_support_styles' ) ) {
2709 function yatra_block_support_styles() {
2710 // Bail early if function does not exists.
2711 if ( ! function_exists( 'wp_style_engine_get_stylesheet_from_context' ) ) {
2712 return;
2713 }
2714
2715 $core_styles_keys = array( 'block-supports' );
2716
2717 $compiled_core_stylesheet = '';
2718
2719 foreach ( $core_styles_keys as $style_key ) {
2720 $compiled_core_stylesheet .= wp_style_engine_get_stylesheet_from_context( $style_key, array() );
2721 }
2722
2723 if ( empty( $compiled_core_stylesheet ) ) {
2724 return;
2725 }
2726
2727 wp_register_style( 'yatra-block-supports', false );
2728 wp_enqueue_style( 'yatra-block-supports' );
2729 wp_add_inline_style( 'yatra-block-supports', $compiled_core_stylesheet );
2730 }
2731 }
2732
2733 if ( ! function_exists( 'yatra_get_footer' ) ) {
2734
2735 function yatra_get_footer( $footer_name = null ) {
2736 global $wp_version;
2737 if (
2738 version_compare( $wp_version, '5.9', '>=' ) &&
2739 function_exists( 'wp_is_block_theme' ) &&
2740 wp_is_block_theme()
2741 ) {
2742 ?>
2743 <footer class="wp-block-template-part site-footer">
2744 <?php block_footer_area(); ?>
2745 </footer>
2746 </div>
2747 <?php yatra_block_support_styles(); ?>
2748 <?php wp_footer(); ?>
2749 </body>
2750 </html>
2751 <?php
2752 } else {
2753 get_footer( $footer_name );
2754 }
2755 }
2756 }
2757
2758 /**
2759 * Render tab icon (supports both SVG icons and images)
2760 *
2761 * @param mixed $icon_data Icon data (string, array, or object)
2762 * @param string $default_icon Default icon name
2763 * @param string $css_class CSS class for the icon
2764 * @param string $label Label for alt text
2765 * @return void Echoes the icon HTML
2766 */
2767 if (!function_exists('yatra_render_tab_icon')) {
2768 function yatra_render_tab_icon($icon_data, $default_icon = 'book', $css_class = '', $label = '') {
2769 if (empty($icon_data)) {
2770 echo function_exists('yatra_svg_icon') ? yatra_svg_icon($default_icon, $css_class) : '';
2771
2772 return;
2773 }
2774 if (is_string($icon_data) && strpos($icon_data, '{') === 0) {
2775 $icon_data = json_decode($icon_data, true);
2776 }
2777 if (is_object($icon_data)) {
2778 $icon_data = (array) $icon_data;
2779 }
2780 if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'image' && !empty($icon_data['value'])) {
2781 $image_url = is_numeric($icon_data['value'])
2782 ? wp_get_attachment_url((int) $icon_data['value'])
2783 : $icon_data['value'];
2784 if ($image_url) {
2785 $size_style = strpos($css_class, 'sticky-nav') !== false ? 'width: 18px; height: 18px;' : 'width: 24px; height: 24px;';
2786 echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($label) . '" class="' . esc_attr($css_class) . '" style="' . esc_attr($size_style) . ' object-fit: cover; border-radius: 4px;">';
2787
2788 return;
2789 }
2790 }
2791 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup built from sanitized picker / SVG registry
2792 echo yatra_stored_picker_icon_markup($icon_data, $default_icon, $css_class);
2793 }
2794 }
2795
2796 if (!function_exists('yatra_listing_sidebar_filter_visible_cap')) {
2797 /**
2798 * How many sidebar checkbox rows to show before "Show more" on the trip listing.
2799 *
2800 * Filter: {@see 'yatra_listing_sidebar_filter_visible_count'} — default 8, clamped 3–40.
2801 *
2802 * @return int
2803 */
2804 function yatra_listing_sidebar_filter_visible_cap(): int
2805 {
2806 $n = (int) apply_filters('yatra_listing_sidebar_filter_visible_count', 8);
2807
2808 return max(3, min(40, $n));
2809 }
2810 }
2811
2812 if (!function_exists('yatra_wishlist_enabled')) {
2813 /**
2814 * Whether wishlist UI and REST should be active (Yatra Pro + setting).
2815 */
2816 function yatra_wishlist_enabled(): bool
2817 {
2818 return \Yatra\Services\SettingsService::wishlistEnabled();
2819 }
2820 }
2821
2822 if (!function_exists('yatra_usage_track_event')) {
2823 /**
2824 * Record an anonymous product telemetry event (requires opt-in).
2825 *
2826 * @param string $event Event key (sanitized).
2827 * @param int $delta Counter increment.
2828 */
2829 function yatra_usage_track_event(string $event, int $delta = 1): void
2830 {
2831 if (!class_exists(\Yatra\Services\StatsUsage::class)) {
2832 return;
2833 }
2834 \Yatra\Services\StatsUsage::instance()->record_event($event, $delta);
2835 }
2836 }
2837