PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Services / SettingsService.php

SettingsService.php in Yatra – Travel Booking & Tour Operator Software 3.0.4, at app/Services/SettingsService.php

885 lines 32.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Services;
6
7 /**
8 * Centralized Settings Service
9 *
10 * Provides a single point of access for all plugin settings.
11 * Caches settings to avoid multiple database queries.
12 *
13 * @package Yatra
14 */
15 class SettingsService
16 {
17 /**
18 * Cached settings
19 */
20 private static ?array $settings = null;
21
22 /**
23 * Cached {@see self::getPermalinkBases()} per request (after {@see 'yatra_permalink_bases'} filter).
24 */
25 private static ?array $permalinkBasesCache = null;
26
27 /**
28 * Settings option prefix in database
29 * Each setting is stored as yatra_{key}
30 */
31 private const OPTION_PREFIX = 'yatra_';
32
33 /**
34 * Default settings
35 */
36 private static array $defaults = [
37 // General
38 'company_name' => '',
39 'company_email' => '',
40 'company_phone' => '',
41 'company_address' => '',
42 'timezone' => 'UTC',
43 'date_format' => 'Y-m-d',
44 'time_format' => 'H:i',
45 /** Primary brand color (hex) for trip/booking/listing frontend — see FrontendThemeCss */
46 'frontend_primary_color' => '#3b82f6',
47 /** Max width for Yatra trip/booking/listing containers (CSS length). Empty = theme.json / content width / filter. */
48 'frontend_container_max_width' => '',
49
50 // Booking
51 'booking_base' => 'book',
52 'use_booking_page' => false,
53 'booking_page_id' => 0,
54 'terms_page_id' => 0,
55 'privacy_policy_page_id' => 0,
56 'enable_guest_booking' => true,
57 'booking_confirmation' => true,
58 'auto_confirm_bookings' => false,
59 'require_login' => false,
60 'allow_guest_checkout' => true,
61 'cancellation_policy' => 'full_refund',
62 'cancellation_days' => 7,
63 'refund_policy' => '',
64 'booking_expiry_hours' => 24,
65 'booking_reminder_days' => 3,
66 'allow_waitlist' => true,
67 'waitlist_auto_confirm' => false,
68 // Pro: when enabled, the single-trip date_specific mode renders a
69 // <select> of available departure dates instead of the flatpickr
70 // calendar (desktop sidebar and mobile sticky bar). Renders no-op
71 // for free installs — see Settings UI + FrontendAssetsProvider gate.
72 'date_picker_as_dropdown' => false,
73
74 // Payment
75 'currency' => 'USD',
76 'payment_test_mode' => true,
77 'currency_position' => 'before',
78 'thousand_separator' => ',',
79 'decimal_separator' => '.',
80 'decimal_places' => 2,
81 // Flexible payments (deposit/partial) - Pro feature
82 // These defaults are overridden by Pro's FlexiblePaymentsModule when active
83 'enable_deposit' => false,
84 'deposit_type' => 'percentage',
85 'deposit_amount' => 20,
86 'deposit_required' => false,
87 'deposit_percentage' => 20,
88 'partial_payment' => false,
89 'partial_payment_percentage' => 30,
90 'auto_confirm_pay_later' => true,
91 'payment_gateways' => ['pay_later'],
92 'payment_methods' => [],
93 'gateway_configs' => [],
94 'gateway_order' => [],
95
96 'allow_save_payment_methods' => false,
97
98 // Email
99 'email_from_name' => '',
100 'email_from_address' => '',
101 'admin_email' => '',
102 'enable_admin_notifications' => true,
103 'enable_customer_notifications' => true,
104
105 // Trip
106 'trip_base' => 'trip',
107 'trips_per_page' => 12,
108 'enable_wishlist' => false,
109 'enable_comparison' => false,
110 'show_sold_out' => true,
111
112 // Customer
113 'enable_customer_accounts' => true,
114 'enable_customer_registration' => true,
115 'customer_account_page' => 0,
116
117 // Review
118 'enable_reviews' => true,
119 'require_booking_to_review' => false,
120 'auto_approve_reviews' => false,
121 'enable_review_moderation' => true,
122 'minimum_rating' => 1,
123 'review_reminder_days' => 7,
124
125 // Tax
126 'enable_tax' => false,
127 'tax_rate' => 0,
128 'tax_inclusive' => false,
129 'tax_label' => 'Tax',
130 'multiple_taxes_enabled' => false,
131 'multiple_taxes' => [],
132 'multiple_taxes_by_country' => [],
133
134 // Currency
135 'enabled_currencies' => ['USD'],
136 'default_currency' => 'USD',
137
138 // Notification
139 'enable_push_notifications' => false,
140 'enable_sms_notifications' => false,
141
142 // Permalink
143 'destination_base' => 'destination',
144 'activity_base' => 'activity',
145 'trip_category_base' => 'trip-category',
146
147 // Advanced
148 'enable_debug_mode' => false,
149 'delete_data_on_uninstall' => false,
150
151 // Booking Form Builder
152 'booking_form_config' => [],
153 ];
154
155 /**
156 * Get default booking form configuration
157 *
158 * @return array
159 */
160 public static function getDefaultBookingFormConfig(): array
161 {
162 return [
163 'contact_form' => [
164 'title' => 'Lead Traveler / Contact Information',
165 'description' => 'Primary contact person for this booking',
166 'fields' => [
167 ['id' => 'first_name', 'type' => 'text', 'label' => 'First Name', 'placeholder' => 'Enter first name', 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half', 'locked' => true],
168 ['id' => 'last_name', 'type' => 'text', 'label' => 'Last Name', 'placeholder' => 'Enter last name', 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half', 'locked' => true],
169 ['id' => 'email', 'type' => 'email', 'label' => 'Email Address', 'placeholder' => 'your@email.com', 'required' => true, 'enabled' => true, 'order' => 3, 'width' => 'half', 'locked' => true],
170 ['id' => 'phone', 'type' => 'tel', 'label' => 'Phone Number', 'placeholder' => '+1 234 567 8900', 'required' => true, 'enabled' => true, 'order' => 4, 'width' => 'half', 'locked' => true],
171 ['id' => 'country', 'type' => 'country', 'label' => 'Country', 'placeholder' => 'Select Country', 'required' => true, 'enabled' => true, 'order' => 5, 'width' => 'half', 'locked' => true],
172 ['id' => 'nationality', 'type' => 'country', 'label' => 'Nationality', 'placeholder' => 'Select Nationality', 'required' => false, 'enabled' => true, 'order' => 6, 'width' => 'half'],
173 ['id' => 'address', 'type' => 'text', 'label' => 'Address', 'placeholder' => 'Street address (optional)', 'required' => false, 'enabled' => true, 'order' => 7, 'width' => 'full'],
174 ],
175 ],
176 'emergency_contact_form' => [
177 'title' => 'Emergency Contact',
178 'description' => 'Person to contact in case of emergency',
179 'enabled' => true,
180 'fields' => [
181 ['id' => 'name', 'type' => 'text', 'label' => 'Contact Name', 'placeholder' => 'Full name', 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half'],
182 ['id' => 'phone', 'type' => 'tel', 'label' => 'Contact Phone', 'placeholder' => '+1 234 567 8900', 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half'],
183 ['id' => 'relationship', 'type' => 'select', 'label' => 'Relationship', 'placeholder' => 'Select Relationship', 'required' => false, 'enabled' => true, 'order' => 3, 'width' => 'full', 'options' => [
184 ['value' => 'spouse', 'label' => 'Spouse/Partner'],
185 ['value' => 'parent', 'label' => 'Parent'],
186 ['value' => 'sibling', 'label' => 'Sibling'],
187 ['value' => 'child', 'label' => 'Child'],
188 ['value' => 'friend', 'label' => 'Friend'],
189 ['value' => 'other', 'label' => 'Other'],
190 ]],
191 ],
192 ],
193 'traveler_form' => [
194 'title' => 'Traveler Information',
195 'description' => 'Please provide details for each traveler',
196 'fields' => [
197 ['id' => 'first_name', 'type' => 'text', 'label' => 'First Name', 'placeholder' => 'Legal first name', 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half'],
198 ['id' => 'last_name', 'type' => 'text', 'label' => 'Last Name', 'placeholder' => 'Legal last name', 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half'],
199 ['id' => 'date_of_birth', 'type' => 'date', 'label' => 'Date of Birth', 'placeholder' => '', 'required' => true, 'enabled' => true, 'order' => 3, 'width' => 'half'],
200 ['id' => 'gender', 'type' => 'select', 'label' => 'Gender', 'placeholder' => 'Select Gender', 'required' => true, 'enabled' => true, 'order' => 4, 'width' => 'half', 'options' => [
201 ['value' => 'male', 'label' => 'Male'],
202 ['value' => 'female', 'label' => 'Female'],
203 ['value' => 'other', 'label' => 'Other'],
204 ]],
205 ['id' => 'nationality', 'type' => 'country', 'label' => 'Nationality', 'placeholder' => 'Select Nationality', 'required' => true, 'enabled' => true, 'order' => 5, 'width' => 'full'],
206 ['id' => 'dietary', 'type' => 'select', 'label' => 'Dietary Requirements', 'placeholder' => 'Select', 'required' => false, 'enabled' => true, 'order' => 6, 'width' => 'half', 'section' => 'dietary_medical', 'options' => [
207 ['value' => 'none', 'label' => 'No special requirements'],
208 ['value' => 'vegetarian', 'label' => 'Vegetarian'],
209 ['value' => 'vegan', 'label' => 'Vegan'],
210 ['value' => 'halal', 'label' => 'Halal'],
211 ['value' => 'kosher', 'label' => 'Kosher'],
212 ['value' => 'gluten_free', 'label' => 'Gluten Free'],
213 ['value' => 'lactose_free', 'label' => 'Lactose Free'],
214 ['value' => 'other', 'label' => 'Other (specify in notes)'],
215 ]],
216 ['id' => 'medical', 'type' => 'text', 'label' => 'Medical Conditions / Allergies', 'placeholder' => 'Any allergies or conditions we should know', 'required' => false, 'enabled' => true, 'order' => 7, 'width' => 'half', 'section' => 'dietary_medical'],
217 ],
218 ],
219 ];
220 }
221
222 /**
223 * Get booking form configuration (merged with defaults)
224 *
225 * @return array
226 */
227 public static function getBookingFormConfig(): array
228 {
229 $saved_config = self::get('booking_form_config', []);
230 $default_config = self::getDefaultBookingFormConfig();
231
232 // If no saved config, return defaults (Pro may filter)
233 if (empty($saved_config)) {
234 return apply_filters('yatra_booking_form_config', $default_config);
235 }
236
237 // Build a map of locked field IDs from defaults
238 $locked_fields = [];
239 foreach ($default_config as $form_type => $form_config) {
240 if (!empty($form_config['fields'])) {
241 foreach ($form_config['fields'] as $field) {
242 if (!empty($field['locked'])) {
243 $locked_fields[$form_type][$field['id']] = true;
244 }
245 }
246 }
247 }
248
249 // Merge saved with defaults
250 $merged = array_replace_recursive($default_config, $saved_config);
251
252 // Ensure locked status is preserved from defaults (locked cannot be overridden)
253 foreach ($merged as $form_type => &$form_config) {
254 if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
255 foreach ($form_config['fields'] as &$field) {
256 // If this field ID is in the locked list, force locked=true and required=true
257 if (isset($locked_fields[$form_type][$field['id']])) {
258 $field['locked'] = true;
259 $field['required'] = true;
260 }
261 }
262 }
263 }
264
265 return apply_filters('yatra_booking_form_config', $merged);
266 }
267
268 private static function isEmailIdentityKey(string $key): bool
269 {
270 return $key === 'admin_email' || $key === 'from_email' || $key === 'from_name';
271 }
272
273 private static function isEmptyScalar($value): bool
274 {
275 return $value === null || $value === false || $value === ''
276 || (is_string($value) && trim($value) === '');
277 }
278
279 /**
280 * When Yatra delivery options are empty, use WordPress site admin email / blog name (same as installer defaults).
281 *
282 * @param mixed $value
283 * @return mixed
284 */
285 private static function applyEmailIdentityFallback(string $key, $value)
286 {
287 if (!self::isEmailIdentityKey($key) || !self::isEmptyScalar($value)) {
288 return $value;
289 }
290 if ($key === 'from_name') {
291 $wp = (string) get_bloginfo('name');
292
293 return $wp !== '' ? $wp : $value;
294 }
295 $wp = (string) get_option('admin_email', '');
296
297 return $wp !== '' ? $wp : $value;
298 }
299
300 /**
301 * Get all settings
302 *
303 * @return array All settings with defaults applied
304 */
305 public static function all(): array
306 {
307 if (self::$settings === null) {
308 self::load();
309 }
310
311 return self::$settings;
312 }
313
314 /**
315 * Get setting value with fallback to default
316 *
317 * @param string $key Setting key
318 * @param mixed $default Default value if setting not found
319 * @return mixed Setting value or default
320 */
321 public static function get(string $key, $default = null)
322 {
323 if (self::$settings === null) {
324 self::load();
325 }
326
327 if (self::isScheduledPaymentSetting($key)) {
328 $scheduledDefaults = self::scheduledPaymentDefaults();
329
330 return apply_filters(
331 'yatra_scheduled_payment_setting',
332 $default ?? ($scheduledDefaults[$key] ?? null),
333 $key
334 );
335 }
336
337 // Support dot notation for nested access (future use)
338 if (strpos($key, '.') !== false) {
339 $keys = explode('.', $key);
340 $value = self::$settings;
341 foreach ($keys as $k) {
342 if (!isset($value[$k])) {
343 return $default ?? (self::$defaults[$key] ?? null);
344 }
345 $value = $value[$k];
346 }
347 return $value;
348 }
349
350 // If setting exists in cache, return it
351 if (isset(self::$settings[$key])) {
352 return self::applyEmailIdentityFallback($key, self::$settings[$key]);
353 }
354
355 // Try to fetch from database directly for settings not in defaults
356 $option_name = self::OPTION_PREFIX . $key;
357 $value = get_option($option_name, null);
358
359 // Installer / migrations used yatra_email_from_*; REST + EmailService use yatra_from_*.
360 if (($value === null || $value === false || $value === '') && $key === 'from_email') {
361 $legacy = get_option(self::OPTION_PREFIX . 'email_from_address', '');
362 if (is_string($legacy) && $legacy !== '') {
363 $value = $legacy;
364 }
365 }
366 if (($value === null || $value === false || $value === '') && $key === 'from_name') {
367 $legacy = get_option(self::OPTION_PREFIX . 'email_from_name', '');
368 if (is_string($legacy) && $legacy !== '') {
369 $value = $legacy;
370 }
371 }
372
373 if ($value !== null) {
374 // Handle serialized arrays
375 if (is_string($value) && is_serialized($value)) {
376 $value = maybe_unserialize($value);
377 }
378 // Cache the value
379 self::$settings[$key] = $value;
380
381 return self::applyEmailIdentityFallback($key, $value);
382 }
383
384 $fallback = $default ?? (self::$defaults[$key] ?? null);
385
386 return self::applyEmailIdentityFallback($key, $fallback);
387 }
388
389 /**
390 * Check if a boolean setting is enabled
391 *
392 * @param string $key Setting key
393 * @return bool
394 */
395 public static function isEnabled(string $key): bool
396 {
397 // Flexible payment settings require Pro module
398 if (self::isFlexiblePaymentSetting($key)) {
399 $value = apply_filters('yatra_flexible_payment_setting', false, $key);
400 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
401 }
402
403 if (self::isScheduledPaymentSetting($key)) {
404 $defaults = self::scheduledPaymentDefaults();
405 $base = $defaults[$key] ?? false;
406 $value = apply_filters('yatra_scheduled_payment_setting', $base, $key);
407
408 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
409 }
410
411 $value = self::get($key, false);
412 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
413 }
414
415 /**
416 * Get a setting as integer
417 *
418 * @param string $key Setting key
419 * @param int $default Default value
420 * @return int
421 */
422 public static function getInt(string $key, int $default = 0): int
423 {
424 // Flexible payment settings require Pro module
425 if (self::isFlexiblePaymentSetting($key)) {
426 return (int) apply_filters('yatra_flexible_payment_setting', $default, $key);
427 }
428
429 if (self::isScheduledPaymentSetting($key)) {
430 $defaults = self::scheduledPaymentDefaults();
431 $base = $defaults[$key] ?? $default;
432
433 return (int) apply_filters('yatra_scheduled_payment_setting', $base, $key);
434 }
435
436 return (int) self::get($key, $default);
437 }
438
439 /**
440 * Get a setting as float
441 *
442 * @param string $key Setting key
443 * @param float $default Default value
444 * @return float
445 */
446 public static function getFloat(string $key, float $default = 0.0): float
447 {
448 return (float) self::get($key, $default);
449 }
450
451 /**
452 * Get a setting as string
453 *
454 * @param string $key Setting key
455 * @param string $default Default value
456 * @return string
457 */
458 public static function getString(string $key, string $default = ''): string
459 {
460 return (string) self::get($key, $default);
461 }
462
463 /**
464 * Load settings from database
465 * Settings are stored as individual options with yatra_ prefix
466 */
467 private static function load(): void
468 {
469 self::$settings = [];
470
471 // Load each setting from individual options
472 foreach (self::$defaults as $key => $default_value) {
473 $option_name = self::OPTION_PREFIX . $key;
474 $value = get_option($option_name, $default_value);
475
476 // Handle serialized arrays
477 if (is_string($value) && is_serialized($value)) {
478 $value = maybe_unserialize($value);
479 }
480
481 self::$settings[$key] = $value;
482 }
483
484 self::mergeAdminReviewOptionAliases();
485 }
486
487 /**
488 * REST/Settings UI uses yatra_require_booking, yatra_review_moderation, yatra_min_rating;
489 * internal helpers use require_booking_to_review, enable_review_moderation, minimum_rating.
490 */
491 private static function mergeAdminReviewOptionAliases(): void
492 {
493 $map = [
494 'require_booking' => 'require_booking_to_review',
495 'review_moderation' => 'enable_review_moderation',
496 'min_rating' => 'minimum_rating',
497 ];
498 foreach ($map as $adminKey => $internalKey) {
499 $v = get_option(self::OPTION_PREFIX . $adminKey, null);
500 if ($v !== null) {
501 self::$settings[$internalKey] = $v;
502 }
503 }
504 }
505
506 /**
507 * Reload settings (clear cache)
508 */
509 public static function reload(): void
510 {
511 self::$settings = null;
512 self::$permalinkBasesCache = null;
513 self::load();
514 }
515
516 /**
517 * Get default settings
518 *
519 * @return array
520 */
521 public static function getDefaults(): array
522 {
523 return self::$defaults;
524 }
525
526 // =========================================
527 // Convenience Methods for Common Settings
528 // =========================================
529
530 /**
531 * Check if reviews are enabled
532 */
533 public static function reviewsEnabled(): bool
534 {
535 return self::isEnabled('enable_reviews');
536 }
537
538 /**
539 * Check if booking is required for reviews
540 */
541 public static function requireBookingForReview(): bool
542 {
543 return self::isEnabled('require_booking_to_review');
544 }
545
546 /**
547 * Check if reviews auto-approve
548 */
549 public static function autoApproveReviews(): bool
550 {
551 return self::isEnabled('auto_approve_reviews');
552 }
553
554 /**
555 * Check if review moderation is enabled
556 */
557 public static function reviewModerationEnabled(): bool
558 {
559 return self::isEnabled('enable_review_moderation');
560 }
561
562 /**
563 * Get minimum rating allowed
564 */
565 public static function getMinimumRating(): int
566 {
567 return self::getInt('minimum_rating', 1);
568 }
569
570 /**
571 * Get currency settings
572 * Checks both 'currency' and 'default_currency' keys for compatibility
573 * (Admin UI Currency Settings saves as 'default_currency')
574 */
575 public static function getCurrency(): string
576 {
577 // Priority: 'currency' key first (Payment Settings), then 'default_currency' (Currency Settings)
578 $currency = self::getString('currency', '');
579 if (!empty($currency) && $currency !== 'USD') {
580 return $currency;
581 }
582
583 // Check default_currency (from Currency Settings section)
584 $defaultCurrency = self::getString('default_currency', '');
585 if (!empty($defaultCurrency)) {
586 return $defaultCurrency;
587 }
588
589 // Return whatever currency is set, even if USD
590 return !empty($currency) ? $currency : 'USD';
591 }
592
593 /**
594 * Get currency position (before/after)
595 */
596 public static function getCurrencyPosition(): string
597 {
598 return self::getString('currency_position', 'before');
599 }
600
601 /**
602 * Sanitize a single URL path segment used in Yatra rewrites (alphanumeric, underscore, hyphen).
603 */
604 private static function sanitizePermalinkSlug(string $value, string $fallback): string
605 {
606 $v = preg_replace('/[^a-z0-9_-]/i', '', $value);
607
608 return ($v !== '' && is_string($v)) ? $v : $fallback;
609 }
610
611 /**
612 * Default account path slug (before {@see 'yatra_permalink_bases'}).
613 */
614 private static function resolveDefaultAccountBaseSlug(): string
615 {
616 $customerPath = get_option('yatra_customer_account_page', '');
617 if (is_string($customerPath) && $customerPath !== '' && $customerPath !== '0') {
618 $slug = self::slugFromAccountPathString($customerPath);
619 if ($slug !== '') {
620 return self::sanitizePermalinkSlug($slug, 'account');
621 }
622 }
623
624 $base = self::getString('account_base', '');
625 $base = self::sanitizePermalinkSlug($base, '');
626
627 return $base !== '' ? $base : 'account';
628 }
629
630 /**
631 * Raw permalink configuration from options (not yet filtered).
632 *
633 * @return array<string, string>
634 */
635 private static function defaultPermalinkBases(): array
636 {
637 $trip = self::sanitizePermalinkSlug(self::getString('trip_base', 'trip'), 'trip');
638 $booking = self::sanitizePermalinkSlug(self::getString('booking_base', 'booking'), 'booking');
639 $account = self::resolveDefaultAccountBaseSlug();
640 $destination = self::sanitizePermalinkSlug(self::getString('destination_base', 'destination'), 'destination');
641 $activity = self::sanitizePermalinkSlug(self::getString('activity_base', 'activity'), 'activity');
642 $tripCategory = self::sanitizePermalinkSlug(self::getString('trip_category_base', 'trip-category'), 'trip-category');
643
644 return [
645 'trip_base' => $trip,
646 'booking_base' => $booking,
647 'account_base' => $account,
648 'destination_base' => $destination,
649 'activity_base' => $activity,
650 'trip_category_base' => $tripCategory,
651 /** Path segment after booking base for confirmation URLs, e.g. /{booking_base}/confirmation/{ref}/ */
652 'booking_flow_confirmation_segment' => 'confirmation',
653 /** Legacy pageless path /{prefix}/{reference}/ (default kept for old links). */
654 'legacy_booking_confirmation_prefix' => 'booking-confirmation',
655 /** Pageless remaining balance checkout /{prefix}/{token}/ */
656 'remaining_checkout_prefix' => 'remaining-checkout',
657 /** Email verification pretty path /{prefix}/{token}/ */
658 'email_verification_prefix' => 'yatra-verify-email',
659 ];
660 }
661
662 /**
663 * All path segments and prefixes used by Yatra rewrites, routing, and URL helpers.
664 *
665 * Third-party plugins can change slugs in one place via:
666 *
667 * `add_filter( 'yatra_permalink_bases', function ( array $bases ) { $bases['trip_base'] = 'tours'; return $bases; } );`
668 *
669 * **Full URLs (different from bases only):**
670 *
671 * - Outbound links: `yatra_destination_permalink`, `yatra_activity_permalink`, `yatra_category_permalink`, `yatra_trip_permalink`
672 * ({@see yatra_get_destination_permalink()} and siblings in `includes/helpers.php`).
673 * - Inbound path mapping (pretty URLs): {@see \Yatra\Core\Routing\UrlParser::getCleanRequestPath()} filter `yatra_frontend_request_path`.
674 * - Inbound overrides: `yatra_pretty_route_match`, `yatra_plain_route_match` ({@see \Yatra\Core\Routing\PrettyRouteMatcher}, {@see \Yatra\Core\Routing\PlainPageMatcher}).
675 *
676 * After changing bases at runtime you must flush rewrite rules (or bump `yatra_rewrite_rules_version`
677 * in development). Use the {@see 'yatra_register_rewrite_rules'} action to register extra rules that
678 * depend on these bases.
679 *
680 * @return array<string, string>
681 */
682 public static function getPermalinkBases(): array
683 {
684 if (self::$permalinkBasesCache !== null) {
685 return self::$permalinkBasesCache;
686 }
687
688 $defaults = self::defaultPermalinkBases();
689 $filtered = apply_filters('yatra_permalink_bases', $defaults);
690 if (!is_array($filtered)) {
691 $filtered = $defaults;
692 }
693
694 $merged = array_merge($defaults, $filtered);
695 $out = [
696 'trip_base' => self::sanitizePermalinkSlug((string) ($merged['trip_base'] ?? ''), $defaults['trip_base']),
697 'booking_base' => self::sanitizePermalinkSlug((string) ($merged['booking_base'] ?? ''), $defaults['booking_base']),
698 'account_base' => self::sanitizePermalinkSlug((string) ($merged['account_base'] ?? ''), $defaults['account_base']),
699 'destination_base' => self::sanitizePermalinkSlug((string) ($merged['destination_base'] ?? ''), $defaults['destination_base']),
700 'activity_base' => self::sanitizePermalinkSlug((string) ($merged['activity_base'] ?? ''), $defaults['activity_base']),
701 'trip_category_base' => self::sanitizePermalinkSlug((string) ($merged['trip_category_base'] ?? ''), $defaults['trip_category_base']),
702 'booking_flow_confirmation_segment' => self::sanitizePermalinkSlug(
703 (string) ($merged['booking_flow_confirmation_segment'] ?? ''),
704 $defaults['booking_flow_confirmation_segment']
705 ),
706 'legacy_booking_confirmation_prefix' => self::sanitizePermalinkSlug(
707 (string) ($merged['legacy_booking_confirmation_prefix'] ?? ''),
708 $defaults['legacy_booking_confirmation_prefix']
709 ),
710 'remaining_checkout_prefix' => self::sanitizePermalinkSlug(
711 (string) ($merged['remaining_checkout_prefix'] ?? ''),
712 $defaults['remaining_checkout_prefix']
713 ),
714 'email_verification_prefix' => self::sanitizePermalinkSlug(
715 (string) ($merged['email_verification_prefix'] ?? ''),
716 $defaults['email_verification_prefix']
717 ),
718 ];
719
720 self::$permalinkBasesCache = $out;
721
722 return self::$permalinkBasesCache;
723 }
724
725 /**
726 * Get trip base slug
727 */
728 public static function getTripBase(): string
729 {
730 return self::getPermalinkBases()['trip_base'];
731 }
732
733 /**
734 * Get booking base slug
735 */
736 public static function getBookingBase(): string
737 {
738 return self::getPermalinkBases()['booking_base'];
739 }
740
741 /**
742 * URL slug for the customer account area (Settings → Customer → account path).
743 * Derives from yatra_customer_account_page first so routing matches the configured path
744 * even when yatra_account_base was never saved or is out of sync.
745 */
746 public static function getAccountBase(): string
747 {
748 return self::getPermalinkBases()['account_base'];
749 }
750
751 public static function getDestinationBase(): string
752 {
753 return self::getPermalinkBases()['destination_base'];
754 }
755
756 public static function getActivityBase(): string
757 {
758 return self::getPermalinkBases()['activity_base'];
759 }
760
761 public static function getTripCategoryBase(): string
762 {
763 return self::getPermalinkBases()['trip_category_base'];
764 }
765
766 private static function slugFromAccountPathString(string $path): string
767 {
768 $path = trim(str_replace('\\', '/', $path), '/');
769 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
770 $segment = $parts !== [] ? end($parts) : 'account';
771 $slug = sanitize_title($segment);
772
773 return $slug !== '' ? $slug : 'account';
774 }
775
776 /**
777 * Check if using custom booking page
778 */
779 public static function useCustomBookingPage(): bool
780 {
781 return self::isEnabled('use_booking_page') && self::getInt('booking_page_id') > 0;
782 }
783
784 /**
785 * Get booking page ID
786 */
787 public static function getBookingPageId(): int
788 {
789 return self::getInt('booking_page_id', 0);
790 }
791
792 /**
793 * Check if guest booking is allowed
794 */
795 public static function guestBookingEnabled(): bool
796 {
797 return self::isEnabled('enable_guest_booking');
798 }
799
800 /**
801 * Wishlist (saved trips) is a Yatra Pro feature and must be enabled in settings.
802 */
803 public static function wishlistEnabled(): bool
804 {
805 if (!apply_filters('yatra_is_pro_active', false)) {
806 return false;
807 }
808
809 return self::isEnabled('enable_wishlist');
810 }
811
812 /**
813 * Trips per page on front-end listings (aligned with WordPress Reading "posts per page").
814 */
815 public static function getTripsPerPage(): int
816 {
817 if (function_exists('yatra_get_posts_per_page')) {
818 return yatra_get_posts_per_page();
819 }
820
821 return max(1, absint((int) get_option('posts_per_page', 10)));
822 }
823
824 /**
825 * Check if a setting key is a flexible payment setting (Pro feature)
826 *
827 * @param string $key Setting key
828 * @return bool
829 */
830 private static function isFlexiblePaymentSetting(string $key): bool
831 {
832 $flexiblePaymentSettings = [
833 'deposit_required',
834 'deposit_percentage',
835 'partial_payment',
836 'partial_payment_percentage',
837 'enable_deposit',
838 'allow_save_payment_methods',
839 ];
840
841 return in_array($key, $flexiblePaymentSettings, true);
842 }
843
844 /**
845 * Settings owned by Yatra Pro "Scheduled Payments" module (not core options).
846 *
847 * @return array<string, mixed>
848 */
849 private static function scheduledPaymentDefaults(): array
850 {
851 return [
852 'enable_scheduled_payments' => false,
853 'scheduled_payment_type' => 'single',
854 'scheduled_payment_days' => 15,
855 'scheduled_payment_installments' => 1,
856 'scheduled_payment_interval' => 30,
857 'scheduled_payment_reminder_days' => 3,
858 ];
859 }
860
861 private static function isScheduledPaymentSetting(string $key): bool
862 {
863 return array_key_exists($key, self::scheduledPaymentDefaults());
864 }
865
866 /**
867 * Check if flexible payments module is available (Pro active + module enabled)
868 *
869 * @return bool
870 */
871 public static function isFlexiblePaymentsAvailable(): bool
872 {
873 return apply_filters('yatra_flexible_payments_enabled', false);
874 }
875
876 /**
877 * Global payment test/sandbox toggle (Settings → Payment).
878 */
879 public static function isPaymentTestMode(): bool
880 {
881 return self::isEnabled('payment_test_mode');
882 }
883 }
884
885