PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / includes / helpers.php

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

2,372 lines 72.4 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 * Format price with currency
253 *
254 * @param float $amount The amount to format
255 * @param string|null $currency The currency code (optional, uses global setting if not provided)
256 * @param bool $zero_is_unknown When true (default), 0 is shown as "Contact for pricing" (trip/listing).
257 * Set false for checkout, payments, and invoices where 0 is a real amount.
258 * @return string Formatted price
259 */
260 if (!function_exists('yatra_format_price')) {
261 function yatra_format_price(float $amount, ?string $currency = null, bool $zero_is_unknown = true): string
262 {
263 if ($zero_is_unknown && (empty($amount) || $amount == 0)) {
264 return __('Contact for pricing', 'yatra');
265 }
266
267 // Get currency from global settings if not provided
268 if (empty($currency)) {
269 $currency = SettingsService::getCurrency();
270 }
271
272 // Get formatting settings from global settings
273 $currency_position = SettingsService::getCurrencyPosition();
274 $decimal_places = SettingsService::getInt('decimal_places', 2);
275 // Avoid absurd migrated values (e.g. 7+) breaking storefront display; cap at 4.
276 $decimal_places = max(0, min(4, $decimal_places));
277 $thousand_separator = SettingsService::getString('thousand_separator', ',');
278 $decimal_separator = SettingsService::getString('decimal_separator', '.');
279
280 // Format the amount with proper separators
281 $formatted_amount = number_format($amount, $decimal_places, $decimal_separator, $thousand_separator);
282
283 // Get currency symbol
284 $currency_symbol = yatra_get_currency_symbol($currency);
285
286 // Position currency based on settings
287 if ($currency_position === 'right' || $currency_position === 'after') {
288 return $formatted_amount . ' ' . $currency_symbol;
289 }
290
291 return $currency_symbol . ' ' . $formatted_amount;
292 }
293 }
294
295 /**
296 * Get currency symbol from currency code
297 *
298 * @param string $currency_code The currency code (e.g., 'USD', 'EUR', 'NPR')
299 * @return string The currency symbol or code
300 */
301 if (!function_exists('yatra_get_currency_symbol')) {
302 function yatra_get_currency_symbol(string $currency_code): string
303 {
304 $symbols = [
305 'USD' => '$',
306 'EUR' => '',
307 'GBP' => '£',
308 'JPY' => '¥',
309 'CNY' => '¥',
310 'INR' => '',
311 'NPR' => 'Rs',
312 'AUD' => 'A$',
313 'CAD' => 'C$',
314 'CHF' => 'CHF',
315 'NZD' => 'NZ$',
316 'SGD' => 'S$',
317 'HKD' => 'HK$',
318 'KRW' => '',
319 'THB' => '฿',
320 'MYR' => 'RM',
321 'PHP' => '',
322 'IDR' => 'Rp',
323 'VND' => '',
324 'BRL' => 'R$',
325 'MXN' => 'MX$',
326 'RUB' => '',
327 'ZAR' => 'R',
328 'AED' => 'د.إ',
329 'SAR' => '',
330 'TRY' => '',
331 'SEK' => 'kr',
332 'NOK' => 'kr',
333 'DKK' => 'kr',
334 'PLN' => '',
335 'CZK' => '',
336 'HUF' => 'Ft',
337 'ILS' => '',
338 'TWD' => 'NT$',
339 'PKR' => '',
340 'BDT' => '',
341 'LKR' => 'Rs',
342 'EGP' => '',
343 'NGN' => '',
344 'KES' => 'KSh',
345 // Ghanaian cedi – use plain symbol without the GH prefix
346 'GHS' => '',
347 'GHC' => '',
348 'ARS' => 'AR$',
349 'CLP' => 'CL$',
350 'COP' => 'CO$',
351 'PEN' => 'S/',
352 ];
353
354 return $symbols[strtoupper($currency_code)] ?? $currency_code;
355 }
356 }
357
358 /**
359 * Format duration (days/nights)
360 *
361 * @param int $days Number of days
362 * @param int|null $nights Number of nights (optional)
363 * @return string Formatted duration
364 */
365 if (!function_exists('yatra_format_duration')) {
366 function yatra_format_duration(int $days, ?int $nights = null): string
367 {
368 if ($days && $nights) {
369 return $days . ' Days / ' . $nights . ' Nights';
370 } elseif ($days) {
371 return $days . ' Day' . ($days > 1 ? 's' : '');
372 }
373 return 'Flexible';
374 }
375 }
376
377 /**
378 * Render SVG icon
379 *
380 * @param string $icon_name Icon name
381 * @param string $class Optional CSS class
382 * @return string SVG markup
383 */
384 if (!function_exists('yatra_svg_icon')) {
385 function yatra_svg_icon(string $icon_name, string $class = ''): string
386 {
387 static $icons = null;
388
389 // Load icons from JSON file once
390 if ($icons === null) {
391 $icons_file = dirname(__FILE__) . '/icons.json';
392 if (file_exists($icons_file)) {
393 $icons_data = json_decode(file_get_contents($icons_file), true);
394 $icons = [];
395
396 // Convert JSON data to PHP array
397 foreach ($icons_data as $name => $data) {
398 if (isset($data['svg'])) {
399 $icons[$name] = (string) $data['svg'];
400 }
401 }
402 } else {
403 $icons = [];
404 }
405 }
406
407 $svg = $icons[$icon_name] ?? '';
408
409 if ($svg === '' || !is_string($svg)) {
410 return '';
411 }
412
413 if ($class !== '') {
414 $class_attr = esc_attr($class);
415
416 if (preg_match('/<svg[^>]*\sclass="([^"]*)"/i', $svg, $m)) {
417 $existing = trim((string) ($m[1] ?? ''));
418 $merged = trim($existing . ' ' . $class_attr);
419 $svg = preg_replace('/(<svg[^>]*\sclass=")([^"]*)(")/i', '$1' . $merged . '$3', $svg, 1);
420 } else {
421 $svg = preg_replace('/<svg\b/i', '<svg class="' . $class_attr . '"', $svg, 1);
422 }
423 }
424
425 return (string) $svg;
426 }
427 }
428
429 /**
430 * Extract SVG icon slug from a stored icon field (same shape as admin / archive cards).
431 *
432 * @param mixed $icon Raw value from DB (serialized array with type/value, URL, attachment id, or legacy slug string).
433 */
434 function yatra_icon_slug_from_stored_field($icon): string
435 {
436 if ($icon === null || $icon === '') {
437 return '';
438 }
439
440 $icon = maybe_unserialize($icon);
441
442 if (is_array($icon)) {
443 $type = $icon['type'] ?? $icon[0] ?? '';
444 $value = $icon['value'] ?? $icon[1] ?? '';
445 if ($type === 'icon' && !empty($value) && is_string($value)) {
446 return $value;
447 }
448
449 return '';
450 }
451
452 if (is_string($icon)) {
453 if (filter_var($icon, FILTER_VALIDATE_URL)) {
454 return '';
455 }
456 $slug = trim($icon);
457
458 return $slug !== '' ? $slug : '';
459 }
460
461 return '';
462 }
463
464 /**
465 * SVG markup for archive listing CTAs: use admin icon when present and valid in icons.json; else default slug.
466 *
467 * @param string $resolved_icon_slug From the listing loop (same source as card hero icon when type is "icon").
468 * @param string $default_slug icons.json key when no admin icon.
469 */
470 function yatra_archive_listing_cta_icon_markup(string $resolved_icon_slug, string $default_slug, string $class = 'yatra-btn-icon'): string
471 {
472 $slug = trim($resolved_icon_slug);
473 if ($slug !== '' && function_exists('yatra_svg_icon')) {
474 $out = yatra_svg_icon($slug, $class);
475 if ($out !== '') {
476 return $out;
477 }
478 }
479
480 $fallback = trim($default_slug);
481 if ($fallback !== '' && function_exists('yatra_svg_icon')) {
482 return yatra_svg_icon($fallback, $class);
483 }
484
485 return '';
486 }
487
488 /**
489 * Icon slug for trip listing card "View Details" — category, then destination, then difficulty (backend order).
490 *
491 * @param array<int, object|array<string, mixed>> $categories Trip categories from getCategories()
492 * @param array<int, object|array<string, mixed>> $destinations Trip destinations from getDestinations()
493 * @param array<string, mixed> $difficulty From Trip::getDifficulty()
494 */
495 function yatra_trip_listing_card_cta_icon_slug(array $categories, array $destinations, array $difficulty): string
496 {
497 foreach ($categories as $row) {
498 if (empty($row)) {
499 continue;
500 }
501 $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null);
502 $slug = yatra_icon_slug_from_stored_field($raw);
503 if ($slug !== '') {
504 return $slug;
505 }
506 }
507
508 foreach ($destinations as $row) {
509 if (empty($row)) {
510 continue;
511 }
512 $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null);
513 $slug = yatra_icon_slug_from_stored_field($raw);
514 if ($slug !== '') {
515 return $slug;
516 }
517 }
518
519 if (!empty($difficulty['icon']) && is_string($difficulty['icon'])) {
520 $try = trim($difficulty['icon']);
521 if ($try !== '') {
522 return $try;
523 }
524 }
525
526 return '';
527 }
528
529 /**
530 * Get booking base URL slug
531 *
532 * @return string The booking base slug
533 */
534 function yatra_get_booking_base(): string
535 {
536 // Check if using custom booking page
537 if (SettingsService::useCustomBookingPage()) {
538 $booking_page_id = SettingsService::getBookingPageId();
539 if ($booking_page_id > 0) {
540 $page = get_post($booking_page_id);
541 if ($page) {
542 return $page->post_name;
543 }
544 }
545 }
546
547 return SettingsService::getBookingBase();
548 }
549
550 /**
551 * Check if the current page is a booking page
552 *
553 * @return bool
554 */
555 function yatra_is_booking_page(): bool
556 {
557 global $wp_query;
558
559 // Check for custom booking page
560 if (SettingsService::useCustomBookingPage()) {
561 $booking_page_id = SettingsService::getBookingPageId();
562 if ($booking_page_id > 0 && is_page($booking_page_id)) {
563 return true;
564 }
565 }
566
567 $booking_base = SettingsService::getBookingBase();
568 if (!empty($wp_query->get('yatra_page')) && (string) $wp_query->get('yatra_page') === $booking_base) {
569 return true;
570 }
571
572 // Check for dynamic booking URL
573 return !empty($wp_query->get('yatra_booking_trip_slug'));
574 }
575
576 /**
577 * Get the global trip object
578 *
579 * Similar to WordPress get_post(), this function returns the current trip object
580 * when on a single trip page.
581 *
582 * @return object|null The trip object or null if not on a trip page
583 */
584 function yatra_get_trip(): ?object
585 {
586 global $trip;
587 return $trip ?? null;
588 }
589
590 /**
591 * Check if we're on a single trip page
592 *
593 * @return bool True if on a single trip page
594 */
595 function yatra_is_single_trip(): bool
596 {
597
598 global $wp_query;
599 return !empty($wp_query->get('yatra_trip_id'));
600 }
601
602 /**
603 * Get a trip field value with default fallback
604 *
605 * @param string $field The field name
606 * @param mixed $default Default value if field is empty
607 * @return mixed The field value or default
608 */
609 function yatra_get_trip_field(string $field, $default = '')
610 {
611 global $trip;
612
613 if (!$trip || !isset($trip->$field)) {
614 return $default;
615 }
616
617 return $trip->$field ?: $default;
618 }
619
620 /**
621 * Echo a trip field value with escaping
622 *
623 * @param string $field The field name
624 * @param string $escape Escape function: 'html', 'attr', 'url', 'js', 'none'
625 * @param mixed $default Default value if field is empty
626 */
627 function yatra_trip_field(string $field, string $escape = 'html', $default = ''): void
628 {
629 $value = yatra_get_trip_field($field, $default);
630
631 switch ($escape) {
632 case 'html':
633 echo esc_html($value);
634 break;
635 case 'attr':
636 echo esc_attr($value);
637 break;
638 case 'url':
639 echo esc_url($value);
640 break;
641 case 'js':
642 echo esc_js($value);
643 break;
644 case 'none':
645 case 'kses':
646 echo wp_kses_post($value);
647 break;
648 default:
649 echo esc_html($value);
650 }
651 }
652
653 /**
654 * Public URL for the Yatra brand icon (admin menu + React sidebar). Empty if file is missing.
655 */
656 function yatra_get_brand_icon_url(): string
657 {
658 if (!defined('YATRA_PLUGIN_PATH') || !defined('YATRA_PLUGIN_URL')) {
659 return '';
660 }
661
662 $candidates = [
663 'assets/images/yatra-icon.png',
664 'assets/images/yara-icon.png',
665 ];
666
667 foreach ($candidates as $relative) {
668 $file = YATRA_PLUGIN_PATH . $relative;
669 if (!is_readable($file)) {
670 continue;
671 }
672
673 $url = YATRA_PLUGIN_URL . $relative;
674
675 return add_query_arg('ver', (string) filemtime($file), $url);
676 }
677
678 return '';
679 }
680
681 /**
682 * ============================================
683 * BOOKING SESSION MANAGEMENT
684 * ============================================
685 */
686
687 /**
688 * Start WordPress session if not already started
689 */
690 function yatra_start_session(): void
691 {
692 // Start output buffering to prevent accidental output from breaking sessions
693 if (!ob_get_level()) {
694 ob_start();
695 }
696
697 if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
698 // Set session cookie parameters for better compatibility
699 if (PHP_VERSION_ID >= 70300) {
700 session_set_cookie_params([
701 'lifetime' => 0,
702 'path' => defined('COOKIEPATH') ? COOKIEPATH : '/',
703 'domain' => defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '',
704 'secure' => is_ssl(),
705 'httponly' => true,
706 'samesite' => 'Lax'
707 ]);
708 }
709 session_start();
710 }
711 }
712
713 /**
714 * Set booking session data
715 *
716 * @param array $data Booking data to store
717 */
718 function yatra_set_booking_session(array $data): void
719 {
720 yatra_start_session();
721
722 // Clear any existing remaining payment session to avoid conflicts
723 unset($_SESSION['yatra_remaining']);
724
725 $session_data = array_merge(
726 $_SESSION['yatra_booking'] ?? [],
727 $data,
728 ['timestamp' => time()]
729 );
730
731 $_SESSION['yatra_booking'] = $session_data;
732
733 // ALWAYS store in transient as backup (not just for REST API)
734 // This ensures data persists across all request types
735 // Generate or reuse booking token
736 $booking_token = $_SESSION['yatra_booking_token'] ?? 'yatra_booking_' . wp_generate_password(32, false);
737 $_SESSION['yatra_booking_token'] = $booking_token;
738 $session_data['booking_token'] = $booking_token;
739
740 // Store in transient (expires in 30 minutes)
741 try {
742 $transient_set = set_transient($booking_token, $session_data, 1800);
743 } catch (Exception $e) {
744 // Continue without transient - session fallback will be used
745 }
746
747 }
748
749 /**
750 * Get booking session data
751 *
752 * @param string|null $key Specific key to retrieve, or null for all data
753 * @param mixed $default Default value if key not found
754 * @return mixed
755 */
756 function yatra_get_booking_session(?string $key = null, $default = null)
757 {
758 yatra_start_session();
759
760 $booking_data = $_SESSION['yatra_booking'] ?? [];
761
762 // If session is empty, try to restore from transient (REST API → page load transition)
763 if (empty($booking_data) || empty($booking_data['trip_id'])) {
764 // Check for booking token in URL or session
765 $booking_token = $_GET['booking_token'] ?? $_SESSION['yatra_booking_token'] ?? null;
766
767 if ($booking_token) {
768 try {
769 $transient_data = get_transient($booking_token);
770
771 if ($transient_data && is_array($transient_data) && !empty($transient_data['trip_id'])) {
772 // Validate transient data integrity
773 if (isset($transient_data['timestamp']) && (time() - $transient_data['timestamp']) < 1800) {
774 $booking_data = $transient_data;
775 // Restore to session
776 $_SESSION['yatra_booking'] = $booking_data;
777 $_SESSION['yatra_booking_token'] = $booking_token;
778 }
779 }
780 } catch (Exception $e) {
781 // Continue without transient data
782 }
783 }
784 }
785
786 // Check if session is expired (30 minutes)
787 if (!empty($booking_data['timestamp'])) {
788 $session_age = time() - $booking_data['timestamp'];
789 if ($session_age > 1800) { // 30 minutes
790 yatra_clear_booking_session();
791 return $key ? $default : [];
792 }
793 }
794
795 if ($key === null) {
796 return $booking_data;
797 }
798
799 return $booking_data[$key] ?? $default;
800 }
801
802 /**
803 * Clear booking session data (PHP session, booking token, and REST backup transient).
804 *
805 * Without removing the token and transient, yatra_get_booking_session() can repopulate
806 * checkout data from the transient on the next request after a completed booking.
807 */
808 function yatra_clear_booking_session(): void
809 {
810 yatra_start_session();
811
812 $token = $_SESSION['yatra_booking_token'] ?? null;
813 if (is_string($token) && $token !== '') {
814 delete_transient($token);
815 }
816
817 unset($_SESSION['yatra_booking'], $_SESSION['yatra_booking_token']);
818 }
819
820 /**
821 * Check if booking session exists and is valid
822 *
823 * @return bool
824 */
825 function yatra_has_booking_session(): bool
826 {
827 $booking_data = yatra_get_booking_session();
828 return !empty($booking_data) && !empty($booking_data['trip_id']);
829 }
830
831 /**
832 * ============================================
833 * REMAINING PAYMENT SESSION MANAGEMENT
834 * ============================================
835 */
836
837 /**
838 * Set remaining payment session data
839 *
840 * @param array $data Remaining payment data to store
841 */
842 function yatra_set_remaining_session(array $data): void
843 {
844 yatra_start_session();
845
846 // Clear checkout session fully (including token + transient) before remaining-payment flow
847 yatra_clear_booking_session();
848
849 $_SESSION['yatra_remaining'] = array_merge(
850 $data,
851 ['timestamp' => time()]
852 );
853
854 // Ensure session data is written to storage immediately
855 if (session_status() === PHP_SESSION_ACTIVE) {
856 session_write_close();
857 }
858 }
859
860 /**
861 * Get remaining payment session data
862 *
863 * @param string|null $key Specific key to retrieve, or null for all data
864 * @param mixed $default Default value if key not found
865 * @return mixed
866 */
867 function yatra_get_remaining_session(?string $key = null, $default = null)
868 {
869 yatra_start_session();
870
871 $remaining_data = $_SESSION['yatra_remaining'] ?? [];
872
873 // Check if session is expired (30 minutes)
874 if (!empty($remaining_data['timestamp'])) {
875 $session_age = time() - $remaining_data['timestamp'];
876 if ($session_age > 1800) { // 30 minutes
877 yatra_clear_remaining_session();
878 return $key ? $default : [];
879 }
880 }
881
882 if ($key === null) {
883 return $remaining_data;
884 }
885
886 return $remaining_data[$key] ?? $default;
887 }
888
889 /**
890 * Clear remaining payment session data
891 */
892 function yatra_clear_remaining_session(): void
893 {
894 yatra_start_session();
895 unset($_SESSION['yatra_remaining']);
896 }
897
898 /**
899 * Check if remaining payment session exists and is valid
900 *
901 * @return bool
902 */
903 function yatra_has_remaining_session(): bool
904 {
905 $remaining_data = yatra_get_remaining_session();
906 return !empty($remaining_data) && !empty($remaining_data['booking_id']);
907 }
908
909 /**
910 * Get the active checkout session type
911 *
912 * @return string|null 'remaining' if remaining session exists, 'booking' if booking session exists, null if neither
913 */
914 function yatra_get_checkout_session_type(): ?string
915 {
916 if (yatra_has_remaining_session()) {
917 return 'remaining';
918 }
919
920 if (yatra_has_booking_session()) {
921 return 'booking';
922 }
923
924 return null;
925 }
926
927 /**
928 * Get the active checkout session data (remaining or booking)
929 *
930 * @return array Session data with 'type' key indicating session type
931 */
932 function yatra_get_active_checkout_session(): array
933 {
934 if (yatra_has_remaining_session()) {
935 $data = yatra_get_remaining_session();
936 $data['session_type'] = 'remaining';
937 return $data;
938 }
939
940 if (yatra_has_booking_session()) {
941 $data = yatra_get_booking_session();
942 $data['session_type'] = 'booking';
943 return $data;
944 }
945
946 return [];
947 }
948
949 /**
950 * Get booking/checkout URL
951 *
952 * Logic:
953 * 1. If custom booking page is set → return that page's URL
954 * 2. Otherwise → return dynamic URL using booking_base from settings (e.g., /bookings/)
955 *
956 * @return string Booking URL
957 */
958 function yatra_get_checkout_url(): string
959 {
960 $permalink_structure = get_option('permalink_structure');
961 $is_plain = empty($permalink_structure);
962
963 // Check if custom booking page is set via SettingsService
964 if (SettingsService::useCustomBookingPage()) {
965 $page_id = SettingsService::getBookingPageId();
966 if ($page_id > 0) {
967 return get_permalink($page_id);
968 }
969 }
970
971 // Default dynamic URL using booking base from settings
972 $base = SettingsService::getBookingBase();
973 if ($is_plain) {
974 return add_query_arg(['yatra_page' => $base], home_url('/'));
975 }
976
977 return home_url('/' . $base . '/');
978 }
979
980 /**
981 * Front-end URL for booking confirmation for a given reference.
982 *
983 * Booking confirmation is pageless: Yatra serves it via rewrite rules and query vars,
984 * not a WordPress page permalink. Pretty URLs use /{booking_base}/confirmation/{reference}/.
985 * Plain permalinks use ?yatra_booking_confirmation={reference}.
986 *
987 * To use a real WordPress page as the base (advanced), filter {@see 'yatra_booking_confirmation_base_url'}.
988 * Legacy /booking-confirmation/{reference}/ remains registered in rewrites for old links.
989 *
990 * @param string $reference Booking reference segment (may be empty for base URL only).
991 * @return string Full URL.
992 */
993 function yatra_get_booking_confirmation_url(string $reference = ''): string
994 {
995 $reference = (string) $reference;
996 $permalink_structure = get_option('permalink_structure');
997 $is_plain = empty($permalink_structure);
998
999 if ($is_plain) {
1000 if ($reference === '') {
1001 $url = home_url('/');
1002 } else {
1003 $url = add_query_arg('yatra_booking_confirmation', $reference, home_url('/'));
1004 }
1005 } else {
1006 $booking_base = trim((string) SettingsService::getBookingBase(), '/');
1007 if ($booking_base === '') {
1008 $booking_base = 'book';
1009 }
1010 $virtual_base = home_url('/' . $booking_base . '/confirmation/');
1011
1012 /**
1013 * Override the base URL for booking confirmation (before the reference path segment).
1014 * Return a non-empty string to use a custom base (e.g. get_permalink( $page_id )).
1015 * Default null keeps the pageless virtual URL from Settings → booking base.
1016 *
1017 * @param string|null $base_url Custom base, or null to use virtual URL.
1018 * @param string $reference Booking reference (may be empty).
1019 */
1020 $base_url = apply_filters('yatra_booking_confirmation_base_url', null, $reference);
1021 if (!is_string($base_url) || $base_url === '') {
1022 $base_url = $virtual_base;
1023 }
1024
1025 if ($reference === '') {
1026 $url = trailingslashit($base_url);
1027 } else {
1028 $url = trailingslashit($base_url) . $reference . '/';
1029 }
1030 }
1031
1032 /**
1033 * Filter the booking confirmation URL.
1034 *
1035 * @param string $url Built URL.
1036 * @param string $reference Booking reference (may be empty).
1037 */
1038 return (string) apply_filters('yatra_booking_confirmation_url', $url, $reference);
1039 }
1040
1041 /**
1042 * ============================================
1043 * ARCHIVE LISTING (plain permalinks pagination)
1044 * ============================================
1045 */
1046
1047 /**
1048 * Items per page from WordPress Reading settings ("Blog pages show at most").
1049 * Used for Yatra front-end listings (trips, taxonomies, activity/destination/category archives).
1050 *
1051 * @return int At least 1.
1052 */
1053 function yatra_get_posts_per_page(): int
1054 {
1055 $n = absint((int) get_option('posts_per_page', 10));
1056
1057 return (int) apply_filters('yatra_posts_per_page', max(1, $n));
1058 }
1059
1060 /**
1061 * Current page number for Yatra archive templates (activity, destination, trip category).
1062 * Handles plain URLs where WordPress may use {@see 'paged'} or {@see 'page'} on the front page.
1063 */
1064 function yatra_get_archive_listing_paged(): int
1065 {
1066 if (isset($_GET['paged']) && $_GET['paged'] !== '') {
1067 return max(1, absint(wp_unslash($_GET['paged'])));
1068 }
1069
1070 if (!empty($_GET['yatra_page']) && isset($_GET['page']) && $_GET['page'] !== '') {
1071 return max(1, absint(wp_unslash($_GET['page'])));
1072 }
1073
1074 $p = (int) get_query_var('paged');
1075 if ($p > 0) {
1076 return max(1, $p);
1077 }
1078
1079 $p = (int) get_query_var('page');
1080
1081 return max(1, $p);
1082 }
1083
1084 /**
1085 * Result summary for destination / activity / trip-category browse pages (parity with trip grid header).
1086 *
1087 * @param string $items_label Plural noun, e.g. translated "destinations".
1088 */
1089 function yatra_archive_browse_results_line(int $start, int $end, int $total, int $page, int $pages, string $items_label): string
1090 {
1091 if ($total <= 0) {
1092 return '';
1093 }
1094
1095 return sprintf(
1096 /* translators: 1–2: range, 3: total, 4: item type, 5–6: pagination */
1097 __('Showing %1$d–%2$d of %3$d %4$s (page %5$d of %6$d)', 'yatra'),
1098 $start,
1099 $end,
1100 $total,
1101 $items_label,
1102 $page,
1103 $pages
1104 );
1105 }
1106
1107 /**
1108 * Request path (leading slash, no query string) for same-page links. Strips /page/N/ pagination segments.
1109 */
1110 function yatra_get_current_request_path_for_query_urls(): string
1111 {
1112 $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
1113 $base_path = strtok($request_uri, '?') ?: '/';
1114 $base_path = rtrim((string) $base_path, '/');
1115 $base_path = preg_replace('#/page/[0-9]+#', '', $base_path);
1116 $base_path = rtrim($base_path, '/');
1117
1118 if ($base_path === '') {
1119 return '/';
1120 }
1121
1122 return $base_path[0] === '/' ? $base_path : '/' . $base_path;
1123 }
1124
1125 /**
1126 * Full URL for the same archive request with a different page (preserves yatra_page and other args).
1127 * Uses the current request path so /destination/, /activity/, /trip-category/ stay on the same listing.
1128 */
1129 function yatra_build_archive_listing_url(int $page_num): string
1130 {
1131 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1132
1133 $qvYatra = (string) get_query_var('yatra_page');
1134 if ($qvYatra !== '' && (!isset($params['yatra_page']) || $params['yatra_page'] === '')) {
1135 $params['yatra_page'] = $qvYatra;
1136 }
1137
1138 if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) {
1139 unset($params['page']);
1140 }
1141
1142 if ($page_num > 1) {
1143 $params['paged'] = (string) $page_num;
1144 } else {
1145 unset($params['paged'], $params['page']);
1146 }
1147
1148 $path = yatra_get_current_request_path_for_query_urls();
1149 $query = http_build_query($params);
1150
1151 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1152 }
1153
1154 /**
1155 * Same request path with a different paged query arg (strips an existing /page/N/ segment first).
1156 * For taxonomy trip lists and other templates not rooted at home_url('/').
1157 */
1158 function yatra_build_current_request_paged_url(int $page_num): string
1159 {
1160 $page_num = max(1, $page_num);
1161 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1162
1163 if ($page_num > 1) {
1164 $params['paged'] = (string) $page_num;
1165 } else {
1166 unset($params['paged'], $params['page']);
1167 }
1168
1169 $path = yatra_get_current_request_path_for_query_urls();
1170 $query = http_build_query($params);
1171
1172 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1173 }
1174
1175 /**
1176 * Same request path with trip sort (TripRepository / TripListingService). Resets pagination.
1177 *
1178 * @param string $sort Allowed: '' (recommended), most_popular, price_low, price_high, rating_high, duration_short, duration_long.
1179 */
1180 function yatra_build_current_request_sort_url(string $sort): string
1181 {
1182 $allowed = ['', 'most_popular', 'price_low', 'price_high', 'rating_high', 'duration_short', 'duration_long'];
1183 if (!in_array($sort, $allowed, true)) {
1184 $sort = '';
1185 }
1186
1187 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1188 unset($params['paged'], $params['page']);
1189 if ($sort !== '') {
1190 $params['sort'] = $sort;
1191 } else {
1192 unset($params['sort']);
1193 }
1194
1195 $path = yatra_get_current_request_path_for_query_urls();
1196 $query = http_build_query($params);
1197
1198 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1199 }
1200
1201 /**
1202 * Compare two archive listing rows (activity, destination, or category) by sort key.
1203 */
1204 function yatra_compare_archive_listing_row_pair(object $a, object $b, string $sort): int
1205 {
1206 $nameA = isset($a->name) ? strtolower((string) $a->name) : '';
1207 $nameB = isset($b->name) ? strtolower((string) $b->name) : '';
1208 $tripsA = isset($a->trips_count) ? (int) $a->trips_count : 0;
1209 $tripsB = isset($b->trips_count) ? (int) $b->trips_count : 0;
1210 $ratingA = isset($a->avg_rating) ? (float) $a->avg_rating : 0.0;
1211 $ratingB = isset($b->avg_rating) ? (float) $b->avg_rating : 0.0;
1212
1213 switch ($sort) {
1214 case 'trips_desc':
1215 return $tripsB <=> $tripsA;
1216 case 'trips_asc':
1217 return $tripsA <=> $tripsB;
1218 case 'name_asc':
1219 return $nameA <=> $nameB;
1220 case 'name_desc':
1221 return $nameB <=> $nameA;
1222 case 'rating_desc':
1223 default:
1224 $cmp = $ratingB <=> $ratingA;
1225 if (0 === $cmp) {
1226 return $tripsB <=> $tripsA;
1227 }
1228
1229 return $cmp;
1230 }
1231 }
1232
1233 /**
1234 * Invokable comparator for {@see yatra_sort_archive_listing_stats_rows()}.
1235 *
1236 * @internal
1237 */
1238 final class Yatra_Archive_Listing_Stats_Comparator
1239 {
1240 /** @var string */
1241 private $sort;
1242
1243 public function __construct(string $sort)
1244 {
1245 $this->sort = $sort;
1246 }
1247
1248 /**
1249 * @param object $a
1250 * @param object $b
1251 */
1252 public function __invoke($a, $b): int
1253 {
1254 return yatra_compare_archive_listing_row_pair($a, $b, $this->sort);
1255 }
1256 }
1257
1258 /**
1259 * Sort archive listing rows in place (stats objects from repository).
1260 */
1261 function yatra_sort_archive_listing_stats_rows(array &$items, string $sort): void
1262 {
1263 if (empty($items)) {
1264 return;
1265 }
1266
1267 usort($items, new Yatra_Archive_Listing_Stats_Comparator($sort));
1268 }
1269
1270 /**
1271 * Sort dropdown URL: same archive, page reset to 1, yatra_sort applied (preserves yatra_page etc.).
1272 */
1273 function yatra_build_archive_listing_sort_url(string $yatra_sort): string
1274 {
1275 $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : [];
1276 unset($params['paged'], $params['page']);
1277 if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) {
1278 unset($params['page']);
1279 }
1280 $params['yatra_sort'] = $yatra_sort;
1281
1282 $path = yatra_get_current_request_path_for_query_urls();
1283 $query = http_build_query($params);
1284
1285 return esc_url($path . ($query !== '' ? '?' . $query : ''));
1286 }
1287
1288 /**
1289 * ============================================
1290 * PERMALINK HELPERS
1291 * ============================================
1292 */
1293
1294 /**
1295 * Get destination permalink
1296 *
1297 * @param object|int $destination Destination object with slug property, or destination ID
1298 * @return string Destination permalink URL
1299 */
1300 function yatra_get_destination_permalink($destination): string
1301 {
1302 if (is_numeric($destination)) {
1303 global $wpdb;
1304 $table = ClassificationsTable::getTableName();
1305 $destination = $wpdb->get_row($wpdb->prepare(
1306 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1307 (int) $destination,
1308 ClassificationTypes::DESTINATION
1309 ));
1310 }
1311
1312 $slug = is_object($destination) ? ($destination->slug ?? '') : '';
1313
1314 if (empty($slug)) {
1315 return '';
1316 }
1317
1318 $base = SettingsService::getString('destination_base', 'destination');
1319 $permalink_structure = get_option('permalink_structure');
1320 $is_plain = empty($permalink_structure);
1321
1322 if ($is_plain) {
1323 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'destination';
1324
1325 return add_query_arg([$key => $slug], home_url('/'));
1326 }
1327
1328 return home_url('/' . $base . '/' . $slug . '/');
1329 }
1330
1331 /**
1332 * Get activity permalink
1333 *
1334 * @param object|int $activity Activity object with slug property, or activity ID
1335 * @return string Activity permalink URL
1336 */
1337 function yatra_get_activity_permalink($activity): string
1338 {
1339 if (is_numeric($activity)) {
1340 global $wpdb;
1341 $table = ClassificationsTable::getTableName();
1342 $activity = $wpdb->get_row($wpdb->prepare(
1343 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1344 (int) $activity,
1345 ClassificationTypes::ACTIVITY
1346 ));
1347 }
1348
1349 $slug = is_object($activity) ? ($activity->slug ?? '') : '';
1350
1351 if (empty($slug)) {
1352 return '';
1353 }
1354
1355 $base = SettingsService::getString('activity_base', 'activity');
1356 $permalink_structure = get_option('permalink_structure');
1357 $is_plain = empty($permalink_structure);
1358
1359 if ($is_plain) {
1360 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'activity';
1361
1362 return add_query_arg([$key => $slug], home_url('/'));
1363 }
1364
1365 return home_url('/' . $base . '/' . $slug . '/');
1366 }
1367
1368 /**
1369 * Get trip category permalink
1370 *
1371 * @param object|int $category Category object with slug property, or category ID
1372 * @return string Category permalink URL
1373 */
1374 function yatra_get_category_permalink($category): string
1375 {
1376 if (is_numeric($category)) {
1377 global $wpdb;
1378 $table = ClassificationsTable::getTableName();
1379 $category = $wpdb->get_row($wpdb->prepare(
1380 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1381 (int) $category,
1382 ClassificationTypes::CATEGORY
1383 ));
1384 }
1385
1386 $slug = is_object($category) ? ($category->slug ?? '') : '';
1387
1388 if (empty($slug)) {
1389 return '';
1390 }
1391
1392 $base = SettingsService::getString('trip_category_base', 'trip-category');
1393 $permalink_structure = get_option('permalink_structure');
1394 $is_plain = empty($permalink_structure);
1395
1396 if ($is_plain) {
1397 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip-category';
1398
1399 return add_query_arg([$key => $slug], home_url('/'));
1400 }
1401
1402 return home_url('/' . $base . '/' . $slug . '/');
1403 }
1404
1405 /**
1406 * Get trip permalink
1407 *
1408 * @param object|int $trip Trip object with slug property, or trip ID
1409 * @return string Trip permalink URL
1410 */
1411 function yatra_get_trip_permalink($trip): string
1412 {
1413 if (is_numeric($trip)) {
1414 global $wpdb;
1415 $table = TripsTable::getTableName();
1416 $trip = $wpdb->get_row($wpdb->prepare(
1417 "SELECT slug FROM {$table} WHERE id = %d",
1418 (int) $trip
1419 ));
1420 }
1421
1422 $slug = is_object($trip) ? ($trip->slug ?? '') : '';
1423
1424 if (empty($slug)) {
1425 return '';
1426 }
1427
1428 $base = SettingsService::getTripBase();
1429 $permalink_structure = get_option('permalink_structure');
1430 $is_plain = empty($permalink_structure);
1431
1432 if ($is_plain) {
1433 $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip';
1434
1435 return add_query_arg([$key => $slug], home_url('/'));
1436 }
1437
1438 return home_url('/' . $base . '/' . $slug . '/');
1439 }
1440
1441 /**
1442 * Canonical URL for the trip archive / filter listing (respects Settings trip base).
1443 * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/.
1444 */
1445 function yatra_get_trip_listing_url(): string
1446 {
1447 $base = SettingsService::getTripBase();
1448 $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'trip';
1449 $permalink_structure = (string) get_option('permalink_structure', '');
1450
1451 if ($permalink_structure === '') {
1452 $url = esc_url(add_query_arg('yatra_page', $base, home_url('/')));
1453 } else {
1454 $url = trailingslashit(home_url('/' . $base . '/'));
1455 }
1456
1457 return (string) apply_filters('yatra_trip_listing_url', $url, $base);
1458 }
1459
1460 /**
1461 * Canonical URL for browse-all taxonomy listings (destinations, activities, trip categories).
1462 * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/.
1463 *
1464 * @param string $listing_type One of: destination, activity, category
1465 */
1466 function yatra_get_taxonomy_listing_url(string $listing_type): string
1467 {
1468 $map = [
1469 'destination' => SettingsService::getString('destination_base', 'destination'),
1470 'activity' => SettingsService::getString('activity_base', 'activity'),
1471 'category' => SettingsService::getString('trip_category_base', 'trip-category'),
1472 ];
1473 $base = $map[$listing_type] ?? '';
1474 $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'destination';
1475 $permalink_structure = (string) get_option('permalink_structure', '');
1476
1477 if ($permalink_structure === '') {
1478 $url = esc_url(add_query_arg('yatra_page', $base, home_url('/')));
1479 } else {
1480 $url = trailingslashit(home_url('/' . $base . '/'));
1481 }
1482
1483 return (string) apply_filters('yatra_taxonomy_listing_url', $url, $listing_type, $base);
1484 }
1485
1486 /**
1487 * Decode trips.price_types for listing-card logic (DB may store JSON string or array).
1488 *
1489 * @return array<int, array<string, mixed>>
1490 */
1491 function yatra_trip_listing_decode_price_types(object $trip): array
1492 {
1493 $pts = $trip->price_types ?? null;
1494 if (is_string($pts) && $pts !== '') {
1495 $decoded = json_decode($pts, true);
1496 $pts = is_array($decoded) ? $decoded : [];
1497 } elseif (!is_array($pts)) {
1498 $pts = [];
1499 }
1500 if ($pts === [] && method_exists($trip, 'getPriceTypes')) {
1501 $got = $trip->getPriceTypes();
1502 $pts = is_array($got) ? $got : [];
1503 }
1504
1505 return $pts;
1506 }
1507
1508 /**
1509 * Lowercase keys for traveler tier labels (used to strip mis-tagged classifications).
1510 *
1511 * @return array<string, true>
1512 */
1513 function yatra_trip_listing_traveler_tier_label_keys(object $trip): array
1514 {
1515 if (($trip->pricing_type ?? '') !== 'traveler_based') {
1516 return [];
1517 }
1518 $keys = [];
1519 foreach (yatra_trip_listing_decode_price_types($trip) as $pt) {
1520 if (!is_array($pt)) {
1521 continue;
1522 }
1523 foreach (['label', 'category_label', 'title'] as $k) {
1524 if (!empty($pt[$k]) && is_string($pt[$k])) {
1525 $t = strtolower(trim($pt[$k]));
1526 if ($t !== '') {
1527 $keys[$t] = true;
1528 }
1529 break;
1530 }
1531 }
1532 }
1533
1534 return $keys;
1535 }
1536
1537 /**
1538 * Ordered unique labels for the listing card “Traveler types” row.
1539 *
1540 * @return list<string>
1541 */
1542 function yatra_trip_listing_traveler_type_labels_for_card(object $trip): array
1543 {
1544 if (($trip->pricing_type ?? '') !== 'traveler_based') {
1545 return [];
1546 }
1547 $labels = [];
1548 $seen = [];
1549 foreach (yatra_trip_listing_decode_price_types($trip) as $pt) {
1550 if (!is_array($pt)) {
1551 continue;
1552 }
1553 foreach (['label', 'category_label', 'title'] as $k) {
1554 if (!empty($pt[$k]) && is_string($pt[$k])) {
1555 $lab = trim($pt[$k]);
1556 if ($lab === '') {
1557 break;
1558 }
1559 $lk = strtolower($lab);
1560 if (!isset($seen[$lk])) {
1561 $seen[$lk] = true;
1562 $labels[] = $lab;
1563 }
1564 break;
1565 }
1566 }
1567 }
1568
1569 return $labels;
1570 }
1571
1572 /**
1573 * Format start → end for listing cards; avoids repeating the same country when both
1574 * strings are "City, Country".
1575 */
1576 function yatra_format_trip_listing_route_line(string $start, string $end): string
1577 {
1578 $start = trim($start);
1579 $end = trim($end);
1580 if ($start === '') {
1581 return $end;
1582 }
1583 if ($end === '') {
1584 return $start;
1585 }
1586 if (strcasecmp($start, $end) === 0) {
1587 return $start;
1588 }
1589 if (strpos($start, ',') !== false && strpos($end, ',') !== false) {
1590 $s_parts = array_map('trim', explode(',', $start, 2));
1591 $e_parts = array_map('trim', explode(',', $end, 2));
1592 if (count($s_parts) === 2 && count($e_parts) === 2
1593 && strcasecmp($s_parts[1], $e_parts[1]) === 0) {
1594 return $s_parts[0] . '' . $e_parts[0] . ', ' . $s_parts[1];
1595 }
1596 }
1597
1598 return $start . '' . $end;
1599 }
1600
1601 /**
1602 * Human label for trip_type column (listing card meta).
1603 */
1604 function yatra_trip_listing_trip_type_label(?string $trip_type): string
1605 {
1606 $t = (string) $trip_type;
1607 $map = [
1608 'single_day' => __('Single day', 'yatra'),
1609 'multi_day' => __('Multi-day', 'yatra'),
1610 'flexible' => __('Flexible', 'yatra'),
1611 ];
1612
1613 return $map[$t] ?? '';
1614 }
1615
1616 /**
1617 * Rating block for listing cards: prefers SQL aggregates (average_rating, review_count)
1618 * when the hydrated reviews array is empty.
1619 *
1620 * @param array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string} $from_reviews
1621 * @return array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string}
1622 */
1623 function yatra_trip_listing_card_rating_data(object $trip, array $from_reviews): array
1624 {
1625 $has = !empty($from_reviews['has_rating']);
1626 $avg = (float) ($from_reviews['average_rating'] ?? 0);
1627 $cnt = (int) ($from_reviews['review_count'] ?? 0);
1628 $fmt = (string) ($from_reviews['formatted_rating'] ?? '0.0');
1629
1630 if ($cnt === 0 || !$has || $avg <= 0) {
1631 $q_avg = isset($trip->average_rating) ? (float) $trip->average_rating : null;
1632 $q_cnt = isset($trip->review_count) ? (int) $trip->review_count : null;
1633 if (($q_cnt === null || $q_cnt === 0) && isset($trip->reviews_count)) {
1634 $q_cnt = (int) $trip->reviews_count;
1635 }
1636 if ($q_cnt !== null && $q_cnt > 0 && $q_avg !== null && $q_avg > 0) {
1637 $avg = round($q_avg, 1);
1638 $cnt = $q_cnt;
1639 $fmt = number_format($avg, 1);
1640 $has = true;
1641 }
1642 }
1643
1644 return [
1645 'has_rating' => $has && $avg > 0 && $cnt > 0,
1646 'average_rating' => $avg,
1647 'review_count' => $cnt,
1648 'formatted_rating' => $fmt,
1649 ];
1650 }
1651
1652 /**
1653 * Avoid repeating the same classification label in the destination, activity, and category
1654 * rows on listing cards (traveler tier labels wrongly linked as classifications, or same
1655 * term attached in multiple roles).
1656 *
1657 * @param array<int, object> $destinations
1658 * @param array<int, object> $activities
1659 * @param array<int, object> $categories
1660 * @return array{0: array<int, object>, 1: array<int, object>, 2: array<int, object>}
1661 */
1662 function yatra_trip_listing_filter_classification_duplicates(array $destinations, array $activities, array $categories, object $trip): array
1663 {
1664 $tier_keys = yatra_trip_listing_traveler_tier_label_keys($trip);
1665
1666 $strip_tiers = static function (array $items) use ($tier_keys): array {
1667 if ($tier_keys === []) {
1668 return $items;
1669 }
1670
1671 return array_values(array_filter($items, static function ($item) use ($tier_keys) {
1672 $n = strtolower(trim((string) ($item->name ?? '')));
1673
1674 return $n === '' || !isset($tier_keys[$n]);
1675 }));
1676 };
1677
1678 $destinations = $strip_tiers($destinations);
1679 $activities = $strip_tiers($activities);
1680 $categories = $strip_tiers($categories);
1681
1682 $seen = [];
1683 $dedupe = static function (array $items) use (&$seen): array {
1684 $out = [];
1685 foreach ($items as $item) {
1686 $n = strtolower(trim((string) ($item->name ?? '')));
1687 if ($n === '') {
1688 $out[] = $item;
1689 continue;
1690 }
1691 if (isset($seen[$n])) {
1692 continue;
1693 }
1694 $seen[$n] = true;
1695 $out[] = $item;
1696 }
1697
1698 return $out;
1699 };
1700
1701 $destinations = $dedupe($destinations);
1702 $activities = $dedupe($activities);
1703 $categories = $dedupe($categories);
1704
1705 return [$destinations, $activities, $categories];
1706 }
1707
1708 /**
1709 * Check if we're on a trip listing page
1710 *
1711 * @return bool True if on a trip listing page
1712 */
1713 function yatra_is_trip_listing(): bool
1714 {
1715 global $yatra_trip_list;
1716
1717 // Check for trip list context (base trip listing page)
1718 if (!empty($yatra_trip_list)) {
1719 return true;
1720 }
1721
1722 // Check if we're on the main trips listing page
1723 $trip_base = SettingsService::getTripBase();
1724 $request_uri = $_SERVER['REQUEST_URI'] ?? '';
1725 $parsed_url = parse_url($request_uri, PHP_URL_PATH);
1726
1727 if ($parsed_url && strpos($parsed_url, '/' . $trip_base) === 0) {
1728 $path_parts = array_values(array_filter(explode('/', trim($parsed_url, '/'))));
1729 if ($path_parts === [] || ($path_parts[0] ?? '') !== $trip_base) {
1730 return false;
1731 }
1732 // /trip/ or /trip/page/2/ (WordPress paged archives)
1733 if (count($path_parts) === 1) {
1734 return true;
1735 }
1736 if (count($path_parts) === 3 && ($path_parts[1] ?? '') === 'page' && ctype_digit((string) ($path_parts[2] ?? ''))) {
1737 return true;
1738 }
1739 }
1740
1741 return false;
1742 }
1743
1744 /**
1745 * Check if we're on a taxonomy page (destination, activity, category)
1746 *
1747 * @return bool True if on a taxonomy page
1748 */
1749 function yatra_is_taxonomy_page(): bool
1750 {
1751 global $yatra_taxonomy_data;
1752 return !empty($yatra_taxonomy_data);
1753 }
1754
1755 /**
1756 * Check if we're on an activity listing page
1757 *
1758 * @return bool True if on an activity listing page
1759 */
1760 function yatra_is_activity_listing(): bool
1761 {
1762 return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'activities';
1763 }
1764
1765 /**
1766 * Check if we're on a destination listing page
1767 *
1768 * @return bool True if on a destination listing page
1769 */
1770 function yatra_is_destination_listing(): bool
1771 {
1772 return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'destinations';
1773 }
1774
1775 /**
1776 * Check if we're on an account page
1777 *
1778 * @return bool True if on an account page
1779 */
1780 function yatra_is_account_page(): bool
1781 {
1782 if (!empty($GLOBALS['yatra_loading_react_account_page'])) {
1783 return true;
1784 }
1785
1786 if ((string) get_query_var('yatra_account_page') !== '') {
1787 return true;
1788 }
1789
1790 global $post;
1791 if ($post && function_exists('has_shortcode') && isset($post->post_content)
1792 && has_shortcode((string) $post->post_content, 'yatra_my_account')) {
1793 return true;
1794 }
1795
1796 if (!$post) {
1797 return false;
1798 }
1799
1800 $accountPageId = get_option('yatra_my_account_page');
1801 return $accountPageId && (int) $post->ID === (int) $accountPageId;
1802 }
1803
1804 /**
1805 * Get difficulty level permalink
1806 *
1807 * @param object|int $difficulty Difficulty object with slug property, or difficulty ID
1808 * @return string Difficulty permalink URL
1809 */
1810 function yatra_get_difficulty_permalink($difficulty): string
1811 {
1812 if (is_numeric($difficulty)) {
1813 global $wpdb;
1814 $table = ClassificationsTable::getTableName();
1815 $difficulty = $wpdb->get_row($wpdb->prepare(
1816 "SELECT slug FROM {$table} WHERE id = %d AND type = %s",
1817 (int) $difficulty,
1818 ClassificationTypes::DIFFICULTY
1819 ));
1820 }
1821
1822 $slug = is_object($difficulty) ? ($difficulty->slug ?? '') : '';
1823
1824 if (empty($slug)) {
1825 return '';
1826 }
1827
1828 $base = SettingsService::getString('difficulty_base', 'difficulty');
1829
1830 return home_url('/' . $base . '/' . $slug . '/');
1831 }
1832
1833 /**
1834 * Load a template file with theme override support
1835 *
1836 * This function allows themes to override plugin templates by placing them in:
1837 * theme/yatra/template-name.php
1838 *
1839 * If no theme override exists, loads from plugin templates directory.
1840 *
1841 * @param string $template_name Template file name (without .php extension)
1842 * @param array $args Arguments to extract and make available in template
1843 * @param string $template_path Template path within plugin (default: 'templates/')
1844 * @param array $data Alternative data array (won't be extracted, available as $data)
1845 * @return void
1846 */
1847 function yatra_get_template(string $template_name, array $args = [], string $template_path = 'templates/', array $data = []): void
1848 {
1849 $template_name = ltrim($template_name, '/');
1850
1851 // Check if theme has override
1852 $theme_template = locate_template([
1853 'yatra/' . $template_name . '.php',
1854 'yatra/' . $template_name
1855 ]);
1856
1857 if ($theme_template) {
1858 // Load from theme
1859 $template_file = $theme_template;
1860 } else {
1861 // Load from plugin
1862 $template_file = YATRA_PLUGIN_PATH . ltrim($template_path, '/') . '/' . $template_name . '.php';
1863 }
1864
1865 // Extract arguments to make them available as individual variables
1866 if (!empty($args)) {
1867 extract($args);
1868 }
1869
1870 // Make data available as $data array (not extracted)
1871 if (!empty($data)) {
1872 $data = $data;
1873 }
1874
1875 // Include the template
1876 if (file_exists($template_file)) {
1877 include $template_file;
1878 }
1879 }
1880
1881 /**
1882 * Enqueue single trip scripts and styles
1883 *
1884 * @return void
1885 */
1886 function yatra_enqueue_single_trip_scripts(): void
1887 {
1888 // Only enqueue on single trip pages
1889 if (!is_single() || get_post_type() !== 'trip') {
1890 return;
1891 }
1892
1893 // Enqueue the single trip JavaScript
1894 wp_enqueue_script(
1895 'yatra-single-trip',
1896 YATRA_PLUGIN_URL . 'assets/js/single-trip.js',
1897 ['jquery'],
1898 YATRA_VERSION,
1899 true
1900 );
1901
1902 // Localize script data
1903 global $trip;
1904 if ($trip) {
1905 wp_localize_script(
1906 'yatra-single-trip',
1907 'yatraSingleTripData',
1908 [
1909 'tripId' => (int) $trip->id,
1910 'basePrice' => (float) ($trip->base_price ?? 0),
1911 'currencySymbol' => yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency()),
1912 'apiUrls' => [
1913 'groupDiscounts' => rest_url('yatra/v1/discounts/group-discounts')
1914 ]
1915 ]
1916 );
1917 }
1918 }
1919
1920 /**
1921 * Calculate base price for single trip display using CalculationService
1922 *
1923 * @param object $trip Trip object
1924 * @return array Pricing data including base_price, has_availability, has_traveler_pricing, pricing_type
1925 */
1926 function yatra_single_trip_calculate_base_price($trip) {
1927 // Check if availability dates exist (PRIORITY)
1928 $has_availability = !empty($trip->availability_dates) && is_array($trip->availability_dates) && count($trip->availability_dates) > 0;
1929
1930 // Determine pricing type from trip settings
1931 $pricing_type = $trip->pricing_type ?? 'regular';
1932 $has_traveler_pricing = ($pricing_type === 'traveler_based' && !empty($trip->price_types));
1933
1934 // Use CalculationService for consistent pricing
1935 $calculationService = new \Yatra\Services\CalculationService();
1936
1937 // Determine base price using CalculationService logic
1938 $trip_price = 0;
1939
1940 if ($has_availability) {
1941 // Get the lowest price from availability dates
1942 $min_price = PHP_FLOAT_MAX;
1943 foreach ($trip->availability_dates as $avail) {
1944 $avail_price = $avail->effective_price ?? $avail->original_price ?? 0;
1945 if ($avail_price > 0 && $avail_price < $min_price) {
1946 $min_price = $avail_price;
1947 }
1948
1949 // Also check price_types within availability if traveler-based
1950 if (!empty($avail->price_types) && is_array($avail->price_types)) {
1951 foreach ($avail->price_types as $pt) {
1952 $pt = (object)$pt;
1953 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
1954 if ($pt_price > 0 && $pt_price < $min_price) {
1955 $min_price = $pt_price;
1956 }
1957 }
1958 }
1959 }
1960
1961 // If no price found from availability, check traveler-based pricing
1962 if ($min_price >= PHP_FLOAT_MAX && $has_traveler_pricing) {
1963 foreach ($trip->price_types as $pt) {
1964 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
1965 if ($pt_price > 0 && $pt_price < $min_price) {
1966 $min_price = $pt_price;
1967 }
1968 }
1969 }
1970
1971 $trip_price = ($min_price < PHP_FLOAT_MAX) ? $min_price : ($trip->sale_price ?: $trip->original_price);
1972 } elseif ($has_traveler_pricing) {
1973 // Get default or first traveler category price
1974 $default_price_type = null;
1975 foreach ($trip->price_types as $pt) {
1976 if (!empty($pt->is_default)) {
1977 $default_price_type = $pt;
1978 break;
1979 }
1980 }
1981 if (!$default_price_type && !empty($trip->price_types)) {
1982 $default_price_type = $trip->price_types[0];
1983 }
1984
1985 // Get the price from the price type - check multiple possible fields
1986 if ($default_price_type) {
1987 $trip_price = 0;
1988 // Try effective_price first, then discounted_price, then original_price
1989 if (!empty($default_price_type->effective_price) && $default_price_type->effective_price > 0) {
1990 $trip_price = (float)$default_price_type->effective_price;
1991 } elseif (!empty($default_price_type->discounted_price) && $default_price_type->discounted_price > 0) {
1992 $trip_price = (float)$default_price_type->discounted_price;
1993 } elseif (!empty($default_price_type->original_price) && $default_price_type->original_price > 0) {
1994 $trip_price = (float)$default_price_type->original_price;
1995 } elseif (!empty($default_price_type->sale_price) && $default_price_type->sale_price > 0) {
1996 $trip_price = (float)$default_price_type->sale_price;
1997 }
1998
1999 // If still no price, try to get the minimum from all price types
2000 if ($trip_price <= 0) {
2001 foreach ($trip->price_types as $pt) {
2002 $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
2003 if ($pt_price > 0 && ($trip_price <= 0 || $pt_price < $trip_price)) {
2004 $trip_price = $pt_price;
2005 }
2006 }
2007 }
2008 } else {
2009 $trip_price = $trip->sale_price ?: $trip->original_price;
2010 }
2011 } else {
2012 // Regular pricing
2013 $trip_price = $trip->sale_price > 0 ? $trip->sale_price : $trip->original_price;
2014 }
2015
2016 // Apply CalculationService filter for dynamic pricing (pro plugins)
2017 $base_price = apply_filters('yatra_calculate_base_amount', $trip_price, [
2018 'trip_price' => $trip_price,
2019 'travelers_count' => 1,
2020 'traveler_counts' => ['default' => 1],
2021 'pricing_type' => $pricing_type,
2022 'price_types' => $trip->price_types ?? [],
2023 'trip_id' => $trip->id ?? 0
2024 ]);
2025
2026 return [
2027 'base_price' => $base_price,
2028 'has_availability' => $has_availability,
2029 'has_traveler_pricing' => $has_traveler_pricing,
2030 'pricing_type' => $pricing_type
2031 ];
2032 }
2033
2034 /**
2035 * Get group discounts data for single trip
2036 *
2037 * @param int $trip_id Trip ID
2038 * @return array Group discounts data including has_group_discounts and group_discounts_data
2039 */
2040 function yatra_single_trip_get_group_discounts($trip_id) {
2041 $has_group_discounts = false;
2042 $group_discounts_data = [];
2043
2044 try {
2045 // Call the group discount API to get detailed discount information
2046 $api_url = rest_url('yatra/v1/discounts/group-discounts');
2047 $response = wp_remote_post($api_url, [
2048 'method' => 'GET',
2049 'body' => [
2050 'trip_ids' => [$trip_id]
2051 ],
2052 'headers' => [
2053 'Content-Type' => 'application/json',
2054 ],
2055 ]);
2056
2057 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2058 $data = json_decode(wp_remote_retrieve_body($response), true);
2059 if (isset($data[$trip_id]) && $data[$trip_id]['has_group_discounts']) {
2060 $has_group_discounts = true;
2061 $group_discounts_data = $data[$trip_id]['discounts'];
2062 }
2063 }
2064 } catch (Exception $e) {
2065 // Silently fail if API call fails - don't break the page
2066 $has_group_discounts = false;
2067 }
2068
2069 return [
2070 'has_group_discounts' => $has_group_discounts,
2071 'group_discounts_data' => $group_discounts_data
2072 ];
2073 }
2074
2075 // Hook into WordPress enqueue system
2076 add_action('wp_enqueue_scripts', 'yatra_enqueue_single_trip_scripts');
2077
2078 // Yatra page type detection functions
2079 if (!function_exists('yatra_is_trip_page')) {
2080 function yatra_is_trip_page() {
2081 global $trip;
2082 return isset($trip) && !empty($trip);
2083 }
2084 }
2085
2086 if (!function_exists('yatra_is_destination_page')) {
2087 function yatra_is_destination_page() {
2088 global $destination, $yatra_taxonomy_data;
2089
2090 // Check direct global first
2091 if (isset($destination) && !empty($destination)) {
2092 return true;
2093 }
2094
2095 // Check taxonomy data
2096 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'destination') {
2097 return true;
2098 }
2099
2100 return false;
2101 }
2102 }
2103
2104 if (!function_exists('yatra_is_activity_page')) {
2105 function yatra_is_activity_page() {
2106 global $activity, $yatra_taxonomy_data;
2107
2108 // Check direct global first
2109 if (isset($activity) && !empty($activity)) {
2110 return true;
2111 }
2112
2113 // Check taxonomy data
2114 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'activity') {
2115 return true;
2116 }
2117
2118 return false;
2119 }
2120 }
2121
2122 if (!function_exists('yatra_is_category_page')) {
2123 function yatra_is_category_page() {
2124 global $category, $yatra_taxonomy_data;
2125
2126 // Check direct global first
2127 if (isset($category) && !empty($category)) {
2128 return true;
2129 }
2130
2131 // Check taxonomy data
2132 if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'category') {
2133 return true;
2134 }
2135
2136 return false;
2137 }
2138 }
2139
2140 if (!function_exists('yatra_is_trip_archive_page')) {
2141 function yatra_is_trip_archive_page() {
2142 $current_url = $_SERVER['REQUEST_URI'] ?? '';
2143 $current_path = parse_url($current_url, PHP_URL_PATH) ?? '';
2144 $trip_base = \Yatra\Services\SettingsService::getTripBase();
2145
2146 // Check for both /trip/ and /trip patterns
2147 $pattern1 = '/' . $trip_base . '/';
2148 $pattern2 = '/' . $trip_base;
2149
2150 return (strpos($current_path, $pattern1) !== false || $current_path === $pattern2) && !yatra_is_trip_page();
2151 }
2152 }
2153
2154 // Yatra only has trip archive pages - no destination/activity/category archive pages
2155
2156 if (!function_exists('yatra_is_listing_page')) {
2157 function yatra_is_listing_page() {
2158 $current_url = $_SERVER['REQUEST_URI'] ?? '';
2159 $current_path = parse_url($current_url, PHP_URL_PATH) ?? '';
2160 return strpos($current_path, '/listing-') !== false;
2161 }
2162 }
2163
2164 if (!function_exists('yatra_is_yatra_page')) {
2165 function yatra_is_yatra_page() {
2166 return yatra_is_trip_page() ||
2167 yatra_is_destination_page() ||
2168 yatra_is_activity_page() ||
2169 yatra_is_category_page() ||
2170 yatra_is_trip_archive_page() ||
2171 yatra_is_listing_page();
2172 }
2173 }
2174
2175 if ( ! function_exists( 'yatra_get_header' ) ) {
2176
2177 function yatra_get_header( $header_name = null ) {
2178 global $wp_version;
2179 if (
2180 version_compare( $wp_version, '5.9', '>=' ) &&
2181 function_exists( 'wp_is_block_theme' ) &&
2182 wp_is_block_theme()
2183 ) {
2184 /*
2185 * Full-site editing themes often omit add_theme_support( 'title-tag' ); the document title is
2186 * injected via template canvas using _block_template_render_title_tag (unconditional). Yatra
2187 * renders this minimal head instead of canvas, so _wp_render_title_tag would no-op and the
2188 * page would have no <title>. Mirror canvas: print title here and drop duplicate core hooks.
2189 */
2190 remove_action( 'wp_head', '_wp_render_title_tag', 1 );
2191 remove_action( 'wp_head', '_block_template_render_title_tag', 1 );
2192 ?>
2193 <!doctype html>
2194 <html <?php language_attributes(); ?>>
2195 <head>
2196 <meta charset="<?php bloginfo( 'charset' ); ?>">
2197 <title><?php echo esc_html( wp_get_document_title() ); ?></title>
2198 <?php wp_head(); ?>
2199 </head>
2200
2201 <body <?php body_class(); ?>>
2202 <?php wp_body_open(); ?>
2203 <div class="wp-site-blocks">
2204 <header class="wp-block-template-part site-header">
2205 <?php block_header_area(); ?>
2206 </header>
2207 <?php
2208 } else {
2209 get_header( $header_name );
2210 }
2211 }
2212 }
2213
2214 if ( ! function_exists( 'yatra_block_support_styles' ) ) {
2215 function yatra_block_support_styles() {
2216 // Bail early if function does not exists.
2217 if ( ! function_exists( 'wp_style_engine_get_stylesheet_from_context' ) ) {
2218 return;
2219 }
2220
2221 $core_styles_keys = array( 'block-supports' );
2222
2223 $compiled_core_stylesheet = '';
2224
2225 foreach ( $core_styles_keys as $style_key ) {
2226 $compiled_core_stylesheet .= wp_style_engine_get_stylesheet_from_context( $style_key, array() );
2227 }
2228
2229 if ( empty( $compiled_core_stylesheet ) ) {
2230 return;
2231 }
2232
2233 wp_register_style( 'yatra-block-supports', false );
2234 wp_enqueue_style( 'yatra-block-supports' );
2235 wp_add_inline_style( 'yatra-block-supports', $compiled_core_stylesheet );
2236 }
2237 }
2238
2239 if ( ! function_exists( 'yatra_get_footer' ) ) {
2240
2241 function yatra_get_footer( $footer_name = null ) {
2242 global $wp_version;
2243 if (
2244 version_compare( $wp_version, '5.9', '>=' ) &&
2245 function_exists( 'wp_is_block_theme' ) &&
2246 wp_is_block_theme()
2247 ) {
2248 ?>
2249 <footer class="wp-block-template-part site-footer">
2250 <?php block_footer_area(); ?>
2251 </footer>
2252 </div>
2253 <?php yatra_block_support_styles(); ?>
2254 <?php wp_footer(); ?>
2255 </body>
2256 </html>
2257 <?php
2258 } else {
2259 get_footer( $footer_name );
2260 }
2261 }
2262 }
2263
2264 /**
2265 * Render tab icon (supports both SVG icons and images)
2266 *
2267 * @param mixed $icon_data Icon data (string, array, or object)
2268 * @param string $default_icon Default icon name
2269 * @param string $css_class CSS class for the icon
2270 * @param string $label Label for alt text
2271 * @return void Echoes the icon HTML
2272 */
2273 if (!function_exists('yatra_render_tab_icon')) {
2274 function yatra_render_tab_icon($icon_data, $default_icon = 'book', $css_class = '', $label = '') {
2275 if (!empty($icon_data)) {
2276 // Handle JSON string that might not be decoded
2277 if (is_string($icon_data) && strpos($icon_data, '{') === 0) {
2278 $icon_data = json_decode($icon_data, true);
2279 }
2280 if (is_array($icon_data) && isset($icon_data['type'])) {
2281 if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) {
2282 // Display image icon
2283 $image_url = is_numeric($icon_data['value'])
2284 ? wp_get_attachment_url($icon_data['value'])
2285 : $icon_data['value'];
2286 if ($image_url) {
2287 $size_style = strpos($css_class, 'sticky-nav') !== false ? 'width: 18px; height: 18px;' : 'width: 24px; height: 24px;';
2288 echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($label) . '" class="' . esc_attr($css_class) . '" style="' . $size_style . ' object-fit: cover; border-radius: 4px;">';
2289 } else {
2290 echo yatra_svg_icon('image', $css_class);
2291 }
2292 } elseif ($icon_data['type'] === 'icon' && !empty($icon_data['value'])) {
2293 // Display SVG icon
2294 echo yatra_svg_icon($icon_data['value'], $css_class);
2295 } else {
2296 // Fallback to default
2297 echo yatra_svg_icon($default_icon, $css_class);
2298 }
2299 } elseif (is_object($icon_data) && isset($icon_data->type)) {
2300 // Handle object format
2301 $icon_array = (array) $icon_data;
2302 if ($icon_array['type'] === 'image' && !empty($icon_array['value'])) {
2303 $image_url = is_numeric($icon_array['value'])
2304 ? wp_get_attachment_url($icon_array['value'])
2305 : $icon_array['value'];
2306 if ($image_url) {
2307 $size_style = strpos($css_class, 'sticky-nav') !== false ? 'width: 18px; height: 18px;' : 'width: 24px; height: 24px;';
2308 echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($label) . '" class="' . esc_attr($css_class) . '" style="' . $size_style . ' object-fit: cover; border-radius: 4px;">';
2309 } else {
2310 echo yatra_svg_icon('image', $css_class);
2311 }
2312 } elseif ($icon_array['type'] === 'icon' && !empty($icon_array['value'])) {
2313 echo yatra_svg_icon($icon_array['value'], $css_class);
2314 } else {
2315 echo yatra_svg_icon($default_icon, $css_class);
2316 }
2317 } elseif (is_string($icon_data)) {
2318 // Direct icon name (backward compatibility)
2319 echo yatra_svg_icon($icon_data, $css_class);
2320 } else {
2321 // Fallback
2322 echo yatra_svg_icon($default_icon, $css_class);
2323 }
2324 } else {
2325 // Default fallback
2326 echo yatra_svg_icon($default_icon, $css_class);
2327 }
2328 }
2329 }
2330
2331 if (!function_exists('yatra_listing_sidebar_filter_visible_cap')) {
2332 /**
2333 * How many sidebar checkbox rows to show before "Show more" on the trip listing.
2334 *
2335 * Filter: {@see 'yatra_listing_sidebar_filter_visible_count'} — default 8, clamped 3–40.
2336 *
2337 * @return int
2338 */
2339 function yatra_listing_sidebar_filter_visible_cap(): int
2340 {
2341 $n = (int) apply_filters('yatra_listing_sidebar_filter_visible_count', 8);
2342
2343 return max(3, min(40, $n));
2344 }
2345 }
2346
2347 if (!function_exists('yatra_wishlist_enabled')) {
2348 /**
2349 * Whether wishlist UI and REST should be active (Yatra Pro + setting).
2350 */
2351 function yatra_wishlist_enabled(): bool
2352 {
2353 return \Yatra\Services\SettingsService::wishlistEnabled();
2354 }
2355 }
2356
2357 if (!function_exists('yatra_usage_track_event')) {
2358 /**
2359 * Record an anonymous product telemetry event (requires opt-in).
2360 *
2361 * @param string $event Event key (sanitized).
2362 * @param int $delta Counter increment.
2363 */
2364 function yatra_usage_track_event(string $event, int $delta = 1): void
2365 {
2366 if (!class_exists(\Yatra\Admin\StatsUsage::class)) {
2367 return;
2368 }
2369 \Yatra\Admin\StatsUsage::instance()->record_event($event, $delta);
2370 }
2371 }
2372