PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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.6, at app/Services/SettingsService.php

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