PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 / app / Services / SettingsService.php

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

1,084 lines 43.2 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 // Blind copy of every outgoing Yatra email, for archiving or monitoring.
117 // Empty (the default) means no copy is sent, so existing sites are
118 // unaffected. Accepts several comma-separated addresses.
119 'email_always_bcc' => '',
120
121 // Email template enable flags.
122 //
123 // These mirror SettingsController::$default_settings + the
124 // entries InstallerService seeds on activation. They're
125 // duplicated here because SettingsService::isEnabled() falls
126 // back to THIS array when the wp_option doesn't exist — and
127 // there are two installation paths where the option is
128 // missing in production:
129 // 1. Sites that upgraded from a Yatra version that didn't
130 // seed the flag (InstallerService runs only on initial
131 // activation, not on update).
132 // 2. Sites whose operator never opened Settings → never
133 // hit the REST save endpoint that would write defaults.
134 // Without this fallback, the verification email + booking
135 // confirmation + every transactional email silently no-ops
136 // on those installs (sendIfEnabled gates on the flag).
137 'email_template_booking' => true,
138 'email_template_confirmation' => true,
139 // Separate "part payment received" email. Off by default: existing sites
140 // keep sending the single payment-received template for every payment,
141 // exactly as before. Only meaningful when deposits / partial payments
142 // are enabled.
143 'email_template_partial_payment' => false,
144 'email_template_cancellation' => true,
145 'email_template_reminder' => true,
146 'email_template_admin_new_booking' => true,
147 'email_template_admin_payment' => true,
148 'email_template_admin_cancellation' => true,
149 'email_template_trip_consent' => true,
150 'email_template_customer_verification' => true,
151 'email_template_guest_verification' => true,
152 'email_template_account_email_change' => true,
153 'email_template_account_email_changed' => true,
154 'email_template_booking_completed' => true,
155 'email_template_booking_expired_customer' => true,
156 'email_template_admin_booking_expired' => true,
157 'email_template_scheduled_payment_reminder' => true,
158 'email_template_scheduled_payment_succeeded' => true,
159 'email_template_scheduled_payment_failed' => true,
160 'email_template_admin_scheduled_payment_failed' => true,
161 'email_template_enquiry_received' => true,
162 'email_template_enquiry_admin' => true,
163 'email_template_enquiry_response' => true,
164 'email_template_review_request' => true,
165 'email_template_abandoned_booking_recovery_first' => true,
166 'email_template_abandoned_booking_recovery_second' => true,
167 'email_template_abandoned_booking_recovery_final' => true,
168 // Customer-registration gate (AuthController::register reads
169 // this exact key). Mismatched name vs InstallerService's
170 // `enable_customer_registration` seed — keeping both names
171 // here so register() works regardless of which key was
172 // saved on prior installs.
173 'customer_registration' => true,
174
175 // Trip
176 'trip_base' => 'trip',
177 'trips_per_page' => 12,
178 'enable_wishlist' => false,
179 'enable_comparison' => false,
180 'show_sold_out' => true,
181
182 // Search & Listing storefront UX.
183 // Search-bar field visibility — default true so the bar renders every
184 // field exactly as before for existing free/pro installs. Owners can
185 // hide individual fields from Settings → Search & Listing.
186 'search_show_keyword' => true,
187 'search_show_destination' => true,
188 'search_show_activities' => true,
189 'search_show_duration' => true,
190 'search_show_budget' => true,
191 // Collapse the listing filter sidebar sections on mobile. Default false
192 // = today's behaviour (all sections expanded on every viewport), so an
193 // existing site sees no change on update until the owner opts in.
194 'collapse_filters_on_mobile' => false,
195
196 // Customer
197 'enable_customer_accounts' => true,
198 'enable_customer_registration' => true,
199 'customer_account_page' => 0,
200
201 // Review
202 'enable_reviews' => true,
203 'require_booking_to_review' => false,
204 'auto_approve_reviews' => false,
205 'enable_review_moderation' => true,
206 'minimum_rating' => 1,
207 'review_reminder_days' => 7,
208
209 // Tax
210 'enable_tax' => false,
211 'tax_rate' => 0,
212 'tax_inclusive' => false,
213 'tax_label' => 'Tax',
214 'multiple_taxes_enabled' => false,
215 'multiple_taxes' => [],
216 'multiple_taxes_by_country' => [],
217
218 // Currency
219 'enabled_currencies' => ['USD'],
220 'default_currency' => 'USD',
221
222 // Notification
223 'enable_push_notifications' => false,
224 'enable_sms_notifications' => false,
225
226 // Permalink
227 'destination_base' => 'destination',
228 'activity_base' => 'activity',
229 'trip_category_base' => 'trip-category',
230
231 // SEO
232 'enable_sitemap' => true,
233
234 // Advanced
235 'enable_debug_mode' => false,
236 'delete_data_on_uninstall' => false,
237
238 // Booking Form Builder
239 'booking_form_config' => [],
240 ];
241
242 /**
243 * Get default booking form configuration
244 *
245 * @return array
246 */
247 public static function getDefaultBookingFormConfig(): array
248 {
249 // User-facing strings (titles, descriptions, labels, placeholders,
250 // option labels) are wrapped in __() so they are (a) extracted into the
251 // .pot for Loco Translate and (b) translated to the active locale when
252 // the config is built — e.g. on a Dutch storefront the default booking
253 // form renders in Dutch. Structural values (id/type/order/width/etc.)
254 // stay literal. Saved/custom labels are additionally translated at
255 // render time (see yatra_translate_form_string()).
256 return [
257 'contact_form' => [
258 'title' => __('Lead Traveler / Contact Information', 'yatra'),
259 'description' => __('Primary contact person for this booking', 'yatra'),
260 'fields' => [
261 ['id' => 'first_name', 'type' => 'text', 'label' => __('First Name', 'yatra'), 'placeholder' => __('Enter first name', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half', 'locked' => true],
262 ['id' => 'last_name', 'type' => 'text', 'label' => __('Last Name', 'yatra'), 'placeholder' => __('Enter last name', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half', 'locked' => true],
263 ['id' => 'email', 'type' => 'email', 'label' => __('Email Address', 'yatra'), 'placeholder' => 'your@email.com', 'required' => true, 'enabled' => true, 'order' => 3, 'width' => 'half', 'locked' => true],
264 ['id' => 'phone', 'type' => 'tel', 'label' => __('Phone Number', 'yatra'), 'placeholder' => '+1 234 567 8900', 'required' => true, 'enabled' => true, 'order' => 4, 'width' => 'half', 'locked' => true],
265 ['id' => 'country', 'type' => 'country', 'label' => __('Country', 'yatra'), 'placeholder' => __('Select Country', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 5, 'width' => 'half', 'locked' => true],
266 ['id' => 'nationality', 'type' => 'country', 'label' => __('Nationality', 'yatra'), 'placeholder' => __('Select Nationality', 'yatra'), 'required' => false, 'enabled' => true, 'order' => 6, 'width' => 'half'],
267 ['id' => 'address', 'type' => 'text', 'label' => __('Address', 'yatra'), 'placeholder' => __('Street address (optional)', 'yatra'), 'required' => false, 'enabled' => true, 'order' => 7, 'width' => 'full'],
268 ],
269 ],
270 'emergency_contact_form' => [
271 'title' => __('Emergency Contact', 'yatra'),
272 'description' => __('Person to contact in case of emergency', 'yatra'),
273 'enabled' => true,
274 'fields' => [
275 ['id' => 'name', 'type' => 'text', 'label' => __('Contact Name', 'yatra'), 'placeholder' => __('Full name', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half'],
276 ['id' => 'phone', 'type' => 'tel', 'label' => __('Contact Phone', 'yatra'), 'placeholder' => '+1 234 567 8900', 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half'],
277 ['id' => 'relationship', 'type' => 'select', 'label' => __('Relationship', 'yatra'), 'placeholder' => __('Select Relationship', 'yatra'), 'required' => false, 'enabled' => true, 'order' => 3, 'width' => 'full', 'options' => [
278 ['value' => 'spouse', 'label' => __('Spouse/Partner', 'yatra')],
279 ['value' => 'parent', 'label' => __('Parent', 'yatra')],
280 ['value' => 'sibling', 'label' => __('Sibling', 'yatra')],
281 ['value' => 'child', 'label' => __('Child', 'yatra')],
282 ['value' => 'friend', 'label' => __('Friend', 'yatra')],
283 ['value' => 'other', 'label' => __('Other', 'yatra')],
284 ]],
285 ],
286 ],
287 'traveler_form' => [
288 'title' => __('Traveler Information', 'yatra'),
289 'description' => __('Please provide details for each traveler', 'yatra'),
290 'fields' => [
291 ['id' => 'first_name', 'type' => 'text', 'label' => __('First Name', 'yatra'), 'placeholder' => __('Legal first name', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 1, 'width' => 'half'],
292 ['id' => 'last_name', 'type' => 'text', 'label' => __('Last Name', 'yatra'), 'placeholder' => __('Legal last name', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 2, 'width' => 'half'],
293 ['id' => 'date_of_birth', 'type' => 'date', 'label' => __('Date of Birth', 'yatra'), 'placeholder' => '', 'required' => true, 'enabled' => true, 'order' => 3, 'width' => 'half'],
294 ['id' => 'gender', 'type' => 'select', 'label' => __('Gender', 'yatra'), 'placeholder' => __('Select Gender', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 4, 'width' => 'half', 'options' => [
295 ['value' => 'male', 'label' => __('Male', 'yatra')],
296 ['value' => 'female', 'label' => __('Female', 'yatra')],
297 ['value' => 'other', 'label' => __('Other', 'yatra')],
298 ]],
299 ['id' => 'nationality', 'type' => 'country', 'label' => __('Nationality', 'yatra'), 'placeholder' => __('Select Nationality', 'yatra'), 'required' => true, 'enabled' => true, 'order' => 5, 'width' => 'full'],
300 ['id' => 'dietary', 'type' => 'select', 'label' => __('Dietary Requirements', 'yatra'), 'placeholder' => __('Select', 'yatra'), 'required' => false, 'enabled' => true, 'order' => 6, 'width' => 'half', 'section' => 'dietary_medical', 'options' => [
301 ['value' => 'none', 'label' => __('No special requirements', 'yatra')],
302 ['value' => 'vegetarian', 'label' => __('Vegetarian', 'yatra')],
303 ['value' => 'vegan', 'label' => __('Vegan', 'yatra')],
304 ['value' => 'halal', 'label' => __('Halal', 'yatra')],
305 ['value' => 'kosher', 'label' => __('Kosher', 'yatra')],
306 ['value' => 'gluten_free', 'label' => __('Gluten Free', 'yatra')],
307 ['value' => 'lactose_free', 'label' => __('Lactose Free', 'yatra')],
308 ['value' => 'other', 'label' => __('Other (specify in notes)', 'yatra')],
309 ]],
310 ['id' => 'medical', 'type' => 'text', 'label' => __('Medical Conditions / Allergies', 'yatra'), 'placeholder' => __('Any allergies or conditions we should know', 'yatra'), 'required' => false, 'enabled' => true, 'order' => 7, 'width' => 'half', 'section' => 'dietary_medical'],
311 ],
312 ],
313 ];
314 }
315
316 /**
317 * Get booking form configuration (merged with defaults)
318 *
319 * @return array
320 */
321 public static function getBookingFormConfig(): array
322 {
323 $saved_config = self::get('booking_form_config', []);
324 $default_config = self::getDefaultBookingFormConfig();
325
326 // If no saved config, return defaults (Pro may filter)
327 if (empty($saved_config)) {
328 return apply_filters('yatra_booking_form_config', $default_config);
329 }
330
331 // Merge saved over defaults. IMPORTANT: `fields` is a positional list,
332 // so a naive array_replace_recursive() merges field-by-INDEX — which
333 // resurrects a deleted default field (saved list is shorter, the tail
334 // default leaks back) and duplicates fields after a middle deletion.
335 // We therefore merge each section's fields BY `id`, treating the saved
336 // config as the authoritative list (order, props, and deletions), while
337 // guaranteeing that locked core fields always exist and stay
338 // locked+required.
339 $merged = [];
340 foreach ($default_config as $form_type => $default_section) {
341 $saved_section = is_array($saved_config[$form_type] ?? null)
342 ? $saved_config[$form_type]
343 : null;
344
345 if ($saved_section === null) {
346 // Section absent from saved config → use the default verbatim.
347 $merged[$form_type] = $default_section;
348 continue;
349 }
350
351 // Section-level scalars (title/description/enabled) come from saved,
352 // falling back to default.
353 $section = array_merge($default_section, $saved_section);
354
355 // Index default fields by id + collect the locked ids for this section.
356 $default_fields_by_id = [];
357 $locked_ids = [];
358 foreach (($default_section['fields'] ?? []) as $df) {
359 if (empty($df['id'])) {
360 continue;
361 }
362 $default_fields_by_id[$df['id']] = $df;
363 if (!empty($df['locked'])) {
364 $locked_ids[$df['id']] = true;
365 }
366 }
367
368 // Rebuild the field list from the saved order, de-duplicated by id.
369 $result_fields = [];
370 $seen = [];
371 $saved_fields = is_array($saved_section['fields'] ?? null)
372 ? $saved_section['fields']
373 : ($default_section['fields'] ?? []);
374 foreach ($saved_fields as $sf) {
375 $id = is_array($sf) ? ($sf['id'] ?? '') : '';
376 if ($id === '' || isset($seen[$id])) {
377 continue; // drop malformed / duplicate field entries
378 }
379 $seen[$id] = true;
380 // Known default field → default props as the base, saved wins.
381 $field = isset($default_fields_by_id[$id])
382 ? array_merge($default_fields_by_id[$id], $sf)
383 : $sf;
384 if (isset($locked_ids[$id])) {
385 $field['locked'] = true;
386 $field['required'] = true;
387 // Locked core fields must keep their original input type — a
388 // saved config can't repurpose them (e.g. to a display-only
389 // text_block), which would drop the real input from checkout.
390 if (isset($default_fields_by_id[$id]['type'])) {
391 $field['type'] = $default_fields_by_id[$id]['type'];
392 }
393 }
394 $result_fields[] = $field;
395 }
396
397 // Locked core fields can never be legitimately removed — re-add any
398 // that the saved config dropped, so checkout/admin always have them.
399 foreach ($locked_ids as $id => $_) {
400 if (!isset($seen[$id])) {
401 $field = $default_fields_by_id[$id];
402 $field['locked'] = true;
403 $field['required'] = true;
404 $result_fields[] = $field;
405 }
406 }
407
408 $section['fields'] = $result_fields;
409 $merged[$form_type] = $section;
410 }
411
412 // Preserve any saved sections that aren't part of the defaults
413 // (future-proofing for Pro-introduced sections).
414 foreach ($saved_config as $form_type => $saved_section) {
415 if (!isset($merged[$form_type])) {
416 $merged[$form_type] = $saved_section;
417 }
418 }
419
420 return apply_filters('yatra_booking_form_config', $merged);
421 }
422
423 private static function isEmailIdentityKey(string $key): bool
424 {
425 return $key === 'admin_email' || $key === 'from_email' || $key === 'from_name';
426 }
427
428 private static function isEmptyScalar($value): bool
429 {
430 return $value === null || $value === false || $value === ''
431 || (is_string($value) && trim($value) === '');
432 }
433
434 /**
435 * When Yatra delivery options are empty, use WordPress site admin email / blog name (same as installer defaults).
436 *
437 * @param mixed $value
438 * @return mixed
439 */
440 private static function applyEmailIdentityFallback(string $key, $value)
441 {
442 if (!self::isEmailIdentityKey($key) || !self::isEmptyScalar($value)) {
443 return $value;
444 }
445 if ($key === 'from_name') {
446 $wp = (string) get_bloginfo('name');
447
448 return $wp !== '' ? $wp : $value;
449 }
450 $wp = (string) get_option('admin_email', '');
451
452 return $wp !== '' ? $wp : $value;
453 }
454
455 /**
456 * Get all settings
457 *
458 * @return array All settings with defaults applied
459 */
460 public static function all(): array
461 {
462 if (self::$settings === null) {
463 self::load();
464 }
465
466 return self::$settings;
467 }
468
469 /**
470 * Get setting value with fallback to default
471 *
472 * @param string $key Setting key
473 * @param mixed $default Default value if setting not found
474 * @return mixed Setting value or default
475 */
476 public static function get(string $key, $default = null)
477 {
478 if (self::$settings === null) {
479 self::load();
480 }
481
482 if (self::isScheduledPaymentSetting($key)) {
483 $scheduledDefaults = self::scheduledPaymentDefaults();
484
485 return apply_filters(
486 'yatra_scheduled_payment_setting',
487 $default ?? ($scheduledDefaults[$key] ?? null),
488 $key
489 );
490 }
491
492 // Support dot notation for nested access (future use)
493 if (strpos($key, '.') !== false) {
494 $keys = explode('.', $key);
495 $value = self::$settings;
496 foreach ($keys as $k) {
497 if (!isset($value[$k])) {
498 return $default ?? (self::$defaults[$key] ?? null);
499 }
500 $value = $value[$k];
501 }
502 return $value;
503 }
504
505 // If setting exists in cache, return it
506 if (isset(self::$settings[$key])) {
507 return self::applyEmailIdentityFallback($key, self::$settings[$key]);
508 }
509
510 // Try to fetch from database directly for settings not in defaults
511 $option_name = self::OPTION_PREFIX . $key;
512 $value = get_option($option_name, null);
513
514 // Installer / migrations used yatra_email_from_*; REST + EmailService use yatra_from_*.
515 if (($value === null || $value === false || $value === '') && $key === 'from_email') {
516 $legacy = get_option(self::OPTION_PREFIX . 'email_from_address', '');
517 if (is_string($legacy) && $legacy !== '') {
518 $value = $legacy;
519 }
520 }
521 if (($value === null || $value === false || $value === '') && $key === 'from_name') {
522 $legacy = get_option(self::OPTION_PREFIX . 'email_from_name', '');
523 if (is_string($legacy) && $legacy !== '') {
524 $value = $legacy;
525 }
526 }
527
528 if ($value !== null) {
529 // Handle serialized arrays
530 if (is_string($value) && is_serialized($value)) {
531 $value = maybe_unserialize($value);
532 }
533 // Cache the value
534 self::$settings[$key] = $value;
535
536 return self::applyEmailIdentityFallback($key, $value);
537 }
538
539 $fallback = $default ?? (self::$defaults[$key] ?? null);
540
541 return self::applyEmailIdentityFallback($key, $fallback);
542 }
543
544 /**
545 * Check if a boolean setting is enabled
546 *
547 * @param string $key Setting key
548 * @return bool
549 */
550 public static function isEnabled(string $key): bool
551 {
552 // Flexible payment settings require Pro module
553 if (self::isFlexiblePaymentSetting($key)) {
554 $value = apply_filters('yatra_flexible_payment_setting', false, $key);
555 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
556 }
557
558 if (self::isScheduledPaymentSetting($key)) {
559 $defaults = self::scheduledPaymentDefaults();
560 $base = $defaults[$key] ?? false;
561 $value = apply_filters('yatra_scheduled_payment_setting', $base, $key);
562
563 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
564 }
565
566 $value = self::get($key, false);
567 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
568 }
569
570 /**
571 * Get a setting as integer
572 *
573 * @param string $key Setting key
574 * @param int $default Default value
575 * @return int
576 */
577 public static function getInt(string $key, int $default = 0): int
578 {
579 // Flexible payment settings require Pro module
580 if (self::isFlexiblePaymentSetting($key)) {
581 return (int) apply_filters('yatra_flexible_payment_setting', $default, $key);
582 }
583
584 if (self::isScheduledPaymentSetting($key)) {
585 $defaults = self::scheduledPaymentDefaults();
586 $base = $defaults[$key] ?? $default;
587
588 return (int) apply_filters('yatra_scheduled_payment_setting', $base, $key);
589 }
590
591 return (int) self::get($key, $default);
592 }
593
594 /**
595 * Get a setting as float
596 *
597 * @param string $key Setting key
598 * @param float $default Default value
599 * @return float
600 */
601 public static function getFloat(string $key, float $default = 0.0): float
602 {
603 return (float) self::get($key, $default);
604 }
605
606 /**
607 * Get a setting as string
608 *
609 * @param string $key Setting key
610 * @param string $default Default value
611 * @return string
612 */
613 public static function getString(string $key, string $default = ''): string
614 {
615 return (string) self::get($key, $default);
616 }
617
618 /**
619 * Load settings from database
620 * Settings are stored as individual options with yatra_ prefix
621 */
622 private static function load(): void
623 {
624 self::$settings = [];
625
626 // Load each setting from individual options
627 foreach (self::$defaults as $key => $default_value) {
628 $option_name = self::OPTION_PREFIX . $key;
629 $value = get_option($option_name, $default_value);
630
631 // Handle serialized arrays
632 if (is_string($value) && is_serialized($value)) {
633 $value = maybe_unserialize($value);
634 }
635
636 self::$settings[$key] = $value;
637 }
638
639 self::mergeAdminReviewOptionAliases();
640 }
641
642 /**
643 * REST/Settings UI uses yatra_require_booking, yatra_review_moderation, yatra_min_rating;
644 * internal helpers use require_booking_to_review, enable_review_moderation, minimum_rating.
645 */
646 private static function mergeAdminReviewOptionAliases(): void
647 {
648 $map = [
649 'require_booking' => 'require_booking_to_review',
650 'review_moderation' => 'enable_review_moderation',
651 'min_rating' => 'minimum_rating',
652 ];
653 foreach ($map as $adminKey => $internalKey) {
654 $v = get_option(self::OPTION_PREFIX . $adminKey, null);
655 if ($v !== null) {
656 self::$settings[$internalKey] = $v;
657 }
658 }
659 }
660
661 /**
662 * Reload settings (clear cache)
663 */
664 public static function reload(): void
665 {
666 self::$settings = null;
667 self::$permalinkBasesCache = null;
668 self::load();
669 }
670
671 /**
672 * Get default settings
673 *
674 * @return array
675 */
676 public static function getDefaults(): array
677 {
678 return self::$defaults;
679 }
680
681 // =========================================
682 // Convenience Methods for Common Settings
683 // =========================================
684
685 /**
686 * Check if reviews are enabled
687 */
688 public static function reviewsEnabled(): bool
689 {
690 return self::isEnabled('enable_reviews');
691 }
692
693 /**
694 * Check if booking is required for reviews
695 */
696 public static function requireBookingForReview(): bool
697 {
698 return self::isEnabled('require_booking_to_review');
699 }
700
701 /**
702 * Check if reviews auto-approve
703 */
704 public static function autoApproveReviews(): bool
705 {
706 return self::isEnabled('auto_approve_reviews');
707 }
708
709 /**
710 * Check if review moderation is enabled
711 */
712 public static function reviewModerationEnabled(): bool
713 {
714 return self::isEnabled('enable_review_moderation');
715 }
716
717 /**
718 * Get minimum rating allowed
719 */
720 public static function getMinimumRating(): int
721 {
722 return self::getInt('minimum_rating', 1);
723 }
724
725 /**
726 * Get currency settings
727 * Checks both 'currency' and 'default_currency' keys for compatibility
728 * (Admin UI Currency Settings saves as 'default_currency')
729 */
730 public static function getCurrency(): string
731 {
732 // Priority: 'currency' key first (Payment Settings), then 'default_currency' (Currency Settings)
733 $currency = self::getString('currency', '');
734 if (!empty($currency) && $currency !== 'USD') {
735 return $currency;
736 }
737
738 // Check default_currency (from Currency Settings section)
739 $defaultCurrency = self::getString('default_currency', '');
740 if (!empty($defaultCurrency)) {
741 return $defaultCurrency;
742 }
743
744 // Return whatever currency is set, even if USD
745 return !empty($currency) ? $currency : 'USD';
746 }
747
748 /**
749 * Get currency position (before/after)
750 */
751 public static function getCurrencyPosition(): string
752 {
753 return self::getString('currency_position', 'before');
754 }
755
756 /**
757 * Single source of truth for the number of decimals shown in prices.
758 *
759 * Historically two unsynced options existed:
760 * - `currency_decimals` — the admin "Number of decimals" field, also handed
761 * to the frontend JS as `decimalPlaces`. Written only when settings are saved.
762 * - `decimal_places` — legacy, written by the installer (default 2) and the
763 * Setup Wizard, and read by {@see yatra_format_price()}.
764 *
765 * They drifted, so PHP-rendered prices (single trip, showcase, listings) and
766 * JS-rendered prices could disagree, and the admin field had no effect on PHP.
767 * This resolver collapses both into ONE value that every reader uses:
768 * 1. the admin field when it has been changed from the default (authoritative);
769 * 2. otherwise a non-default legacy value (preserves Setup-Wizard choices);
770 * 3. otherwise whichever is present, else the default.
771 *
772 * Result is clamped to 0–4. It can never silently regress a site that was
773 * already showing the correct decimals — it only aligns the two readers.
774 */
775 public static function getPriceDecimals(): int
776 {
777 $default = 2;
778
779 $cdRaw = get_option('yatra_currency_decimals', null); // admin field + JS
780 $dpRaw = get_option('yatra_decimal_places', null); // legacy / yatra_format_price
781
782 $cd = ($cdRaw === null || $cdRaw === '') ? null : (int) $cdRaw;
783 $dp = ($dpRaw === null || $dpRaw === '') ? null : (int) $dpRaw;
784
785 if ($cd !== null && $cd !== $default) {
786 $value = $cd; // admin explicitly changed → wins
787 } elseif ($dp !== null && $dp !== $default) {
788 $value = $dp; // legacy Setup-Wizard value → preserved
789 } elseif ($cd !== null) {
790 $value = $cd; // admin field present at default
791 } elseif ($dp !== null) {
792 $value = $dp;
793 } else {
794 $value = $default;
795 }
796
797 return max(0, min(4, $value));
798 }
799
800 /**
801 * Sanitize a single URL path segment used in Yatra rewrites (alphanumeric, underscore, hyphen).
802 */
803 private static function sanitizePermalinkSlug(string $value, string $fallback): string
804 {
805 $v = preg_replace('/[^a-z0-9_-]/i', '', $value);
806
807 return ($v !== '' && is_string($v)) ? $v : $fallback;
808 }
809
810 /**
811 * Default account path slug (before {@see 'yatra_permalink_bases'}).
812 */
813 private static function resolveDefaultAccountBaseSlug(): string
814 {
815 $customerPath = get_option('yatra_customer_account_page', '');
816 if (is_string($customerPath) && $customerPath !== '' && $customerPath !== '0') {
817 $slug = self::slugFromAccountPathString($customerPath);
818 if ($slug !== '') {
819 return self::sanitizePermalinkSlug($slug, 'account');
820 }
821 }
822
823 $base = self::getString('account_base', '');
824 $base = self::sanitizePermalinkSlug($base, '');
825
826 return $base !== '' ? $base : 'account';
827 }
828
829 /**
830 * Raw permalink configuration from options (not yet filtered).
831 *
832 * @return array<string, string>
833 */
834 private static function defaultPermalinkBases(): array
835 {
836 $trip = self::sanitizePermalinkSlug(self::getString('trip_base', 'trip'), 'trip');
837 $booking = self::sanitizePermalinkSlug(self::getString('booking_base', 'booking'), 'booking');
838 $account = self::resolveDefaultAccountBaseSlug();
839 $destination = self::sanitizePermalinkSlug(self::getString('destination_base', 'destination'), 'destination');
840 $activity = self::sanitizePermalinkSlug(self::getString('activity_base', 'activity'), 'activity');
841 $tripCategory = self::sanitizePermalinkSlug(self::getString('trip_category_base', 'trip-category'), 'trip-category');
842
843 return [
844 'trip_base' => $trip,
845 'booking_base' => $booking,
846 'account_base' => $account,
847 'destination_base' => $destination,
848 'activity_base' => $activity,
849 'trip_category_base' => $tripCategory,
850 /** Path segment after booking base for confirmation URLs, e.g. /{booking_base}/confirmation/{ref}/ */
851 'booking_flow_confirmation_segment' => 'confirmation',
852 /** Legacy pageless path /{prefix}/{reference}/ (default kept for old links). */
853 'legacy_booking_confirmation_prefix' => 'booking-confirmation',
854 /** Pageless remaining balance checkout /{prefix}/{token}/ */
855 'remaining_checkout_prefix' => 'remaining-checkout',
856 /** Email verification pretty path /{prefix}/{token}/ */
857 'email_verification_prefix' => 'yatra-verify-email',
858 ];
859 }
860
861 /**
862 * All path segments and prefixes used by Yatra rewrites, routing, and URL helpers.
863 *
864 * Third-party plugins can change slugs in one place via:
865 *
866 * `add_filter( 'yatra_permalink_bases', function ( array $bases ) { $bases['trip_base'] = 'tours'; return $bases; } );`
867 *
868 * **Full URLs (different from bases only):**
869 *
870 * - Outbound links: `yatra_destination_permalink`, `yatra_activity_permalink`, `yatra_category_permalink`, `yatra_trip_permalink`
871 * ({@see yatra_get_destination_permalink()} and siblings in `includes/helpers.php`).
872 * - Inbound path mapping (pretty URLs): {@see \Yatra\Core\Routing\UrlParser::getCleanRequestPath()} filter `yatra_frontend_request_path`.
873 * - Inbound overrides: `yatra_pretty_route_match`, `yatra_plain_route_match` ({@see \Yatra\Core\Routing\PrettyRouteMatcher}, {@see \Yatra\Core\Routing\PlainPageMatcher}).
874 *
875 * After changing bases at runtime you must flush rewrite rules (or bump `yatra_rewrite_rules_version`
876 * in development). Use the {@see 'yatra_register_rewrite_rules'} action to register extra rules that
877 * depend on these bases.
878 *
879 * @return array<string, string>
880 */
881 public static function getPermalinkBases(): array
882 {
883 if (self::$permalinkBasesCache !== null) {
884 return self::$permalinkBasesCache;
885 }
886
887 $defaults = self::defaultPermalinkBases();
888 $filtered = apply_filters('yatra_permalink_bases', $defaults);
889 if (!is_array($filtered)) {
890 $filtered = $defaults;
891 }
892
893 $merged = array_merge($defaults, $filtered);
894 $out = [
895 'trip_base' => self::sanitizePermalinkSlug((string) ($merged['trip_base'] ?? ''), $defaults['trip_base']),
896 'booking_base' => self::sanitizePermalinkSlug((string) ($merged['booking_base'] ?? ''), $defaults['booking_base']),
897 'account_base' => self::sanitizePermalinkSlug((string) ($merged['account_base'] ?? ''), $defaults['account_base']),
898 'destination_base' => self::sanitizePermalinkSlug((string) ($merged['destination_base'] ?? ''), $defaults['destination_base']),
899 'activity_base' => self::sanitizePermalinkSlug((string) ($merged['activity_base'] ?? ''), $defaults['activity_base']),
900 'trip_category_base' => self::sanitizePermalinkSlug((string) ($merged['trip_category_base'] ?? ''), $defaults['trip_category_base']),
901 'booking_flow_confirmation_segment' => self::sanitizePermalinkSlug(
902 (string) ($merged['booking_flow_confirmation_segment'] ?? ''),
903 $defaults['booking_flow_confirmation_segment']
904 ),
905 'legacy_booking_confirmation_prefix' => self::sanitizePermalinkSlug(
906 (string) ($merged['legacy_booking_confirmation_prefix'] ?? ''),
907 $defaults['legacy_booking_confirmation_prefix']
908 ),
909 'remaining_checkout_prefix' => self::sanitizePermalinkSlug(
910 (string) ($merged['remaining_checkout_prefix'] ?? ''),
911 $defaults['remaining_checkout_prefix']
912 ),
913 'email_verification_prefix' => self::sanitizePermalinkSlug(
914 (string) ($merged['email_verification_prefix'] ?? ''),
915 $defaults['email_verification_prefix']
916 ),
917 ];
918
919 self::$permalinkBasesCache = $out;
920
921 return self::$permalinkBasesCache;
922 }
923
924 /**
925 * Get trip base slug
926 */
927 public static function getTripBase(): string
928 {
929 return self::getPermalinkBases()['trip_base'];
930 }
931
932 /**
933 * Get booking base slug
934 */
935 public static function getBookingBase(): string
936 {
937 return self::getPermalinkBases()['booking_base'];
938 }
939
940 /**
941 * URL slug for the customer account area (Settings → Customer → account path).
942 * Derives from yatra_customer_account_page first so routing matches the configured path
943 * even when yatra_account_base was never saved or is out of sync.
944 */
945 public static function getAccountBase(): string
946 {
947 return self::getPermalinkBases()['account_base'];
948 }
949
950 public static function getDestinationBase(): string
951 {
952 return self::getPermalinkBases()['destination_base'];
953 }
954
955 public static function getActivityBase(): string
956 {
957 return self::getPermalinkBases()['activity_base'];
958 }
959
960 public static function getTripCategoryBase(): string
961 {
962 return self::getPermalinkBases()['trip_category_base'];
963 }
964
965 private static function slugFromAccountPathString(string $path): string
966 {
967 $path = trim(str_replace('\\', '/', $path), '/');
968 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
969 $segment = $parts !== [] ? end($parts) : 'account';
970 $slug = sanitize_title($segment);
971
972 return $slug !== '' ? $slug : 'account';
973 }
974
975 /**
976 * Check if using custom booking page
977 */
978 public static function useCustomBookingPage(): bool
979 {
980 return self::isEnabled('use_booking_page') && self::getInt('booking_page_id') > 0;
981 }
982
983 /**
984 * Get booking page ID
985 */
986 public static function getBookingPageId(): int
987 {
988 return self::getInt('booking_page_id', 0);
989 }
990
991 /**
992 * Check if guest booking is allowed
993 */
994 public static function guestBookingEnabled(): bool
995 {
996 return self::isEnabled('enable_guest_booking');
997 }
998
999 /**
1000 * Wishlist (saved trips) is a Yatra Pro feature and must be enabled in settings.
1001 */
1002 public static function wishlistEnabled(): bool
1003 {
1004 if (!apply_filters('yatra_is_pro_active', false)) {
1005 return false;
1006 }
1007
1008 return self::isEnabled('enable_wishlist');
1009 }
1010
1011 /**
1012 * Trips per page on front-end listings (aligned with WordPress Reading "posts per page").
1013 */
1014 public static function getTripsPerPage(): int
1015 {
1016 if (function_exists('yatra_get_posts_per_page')) {
1017 return yatra_get_posts_per_page();
1018 }
1019
1020 return max(1, absint((int) get_option('posts_per_page', 10)));
1021 }
1022
1023 /**
1024 * Check if a setting key is a flexible payment setting (Pro feature)
1025 *
1026 * @param string $key Setting key
1027 * @return bool
1028 */
1029 private static function isFlexiblePaymentSetting(string $key): bool
1030 {
1031 $flexiblePaymentSettings = [
1032 'deposit_required',
1033 'deposit_percentage',
1034 'partial_payment',
1035 'partial_payment_percentage',
1036 'enable_deposit',
1037 'allow_save_payment_methods',
1038 ];
1039
1040 return in_array($key, $flexiblePaymentSettings, true);
1041 }
1042
1043 /**
1044 * Settings owned by Yatra Pro "Scheduled Payments" module (not core options).
1045 *
1046 * @return array<string, mixed>
1047 */
1048 private static function scheduledPaymentDefaults(): array
1049 {
1050 return [
1051 'enable_scheduled_payments' => false,
1052 'scheduled_payment_type' => 'single',
1053 'scheduled_payment_days' => 15,
1054 'scheduled_payment_installments' => 1,
1055 'scheduled_payment_interval' => 30,
1056 'scheduled_payment_reminder_days' => 3,
1057 ];
1058 }
1059
1060 private static function isScheduledPaymentSetting(string $key): bool
1061 {
1062 return array_key_exists($key, self::scheduledPaymentDefaults());
1063 }
1064
1065 /**
1066 * Check if flexible payments module is available (Pro active + module enabled)
1067 *
1068 * @return bool
1069 */
1070 public static function isFlexiblePaymentsAvailable(): bool
1071 {
1072 return apply_filters('yatra_flexible_payments_enabled', false);
1073 }
1074
1075 /**
1076 * Global payment test/sandbox toggle (Settings → Payment).
1077 */
1078 public static function isPaymentTestMode(): bool
1079 {
1080 return self::isEnabled('payment_test_mode');
1081 }
1082 }
1083
1084