PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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 / Controllers / SettingsController.php

SettingsController.php in Yatra – Travel Booking & Tour Operator Software 3.0.7, at app/Controllers/SettingsController.php

1,335 lines 56.1 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\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\EmailTemplatePreviewService;
11
12 /**
13 * Settings REST API Controller
14 * Handles getting and updating plugin settings stored in WordPress options table
15 */
16 class SettingsController extends BaseController
17 {
18 /**
19 * All settings fields with their default values
20 * Pro plugin can add additional settings via filter
21 */
22 private array $default_settings;
23
24 /**
25 * Constructor - initialize default settings with filter
26 */
27 public function __construct()
28 {
29 $wpAdminEmail = (string) get_option('admin_email', '');
30 $wpSiteName = (string) get_bloginfo('name');
31
32 // Define base settings
33 $base_settings = [
34 // General Settings
35 'company_name' => '',
36 'company_email' => '',
37 'company_phone' => '',
38 'company_address' => '',
39 'company_city' => '',
40 'company_state' => '',
41 'company_country' => '',
42 'company_zip' => '',
43 'company_website' => '',
44 'company_logo' => '',
45 'timezone' => 'UTC',
46 'date_format' => 'Y-m-d',
47 'time_format' => 'H:i',
48 'frontend_primary_color' => '#3b82f6',
49 'frontend_container_max_width' => '',
50
51 // Booking Settings
52 'booking_confirmation' => true,
53 'auto_confirm_bookings' => false,
54 'auto_confirm_pay_later' => true,
55 'require_login' => false,
56 'allow_guest_checkout' => true,
57 // cancellation_policy / cancellation_days / refund_policy were
58 // removed in 3.0.5 — they only inserted text into the booking
59 // confirmation email but did NOT enforce a cancellation cutoff
60 // because Yatra has no customer-facing self-service
61 // cancellation flow. Per-trip cancellation copy on the Trip
62 // editor is the supported way to communicate policy. If those
63 // legacy options still exist in wp_options on upgraded sites
64 // they're harmless orphans — the save endpoint no longer
65 // accepts them, and the email template skips the cancellation
66 // paragraph when the global setting is absent.
67 'booking_expiry_hours' => 24,
68 'booking_reminder_days' => 3,
69 'allow_waitlist' => true,
70 'waitlist_auto_confirm' => false,
71 // Pro: render available departure dates as a <select> instead of a
72 // flatpickr calendar on the single-trip sidebar (desktop + mobile).
73 'date_picker_as_dropdown' => false,
74
75 // Payment Settings
76 'currency' => 'USD',
77 'payment_test_mode' => true,
78 'payment_gateways' => [],
79 'payment_methods' => [],
80 'partial_payment' => false,
81 'partial_payment_percentage' => 30,
82 'deposit_required' => false,
83 'deposit_percentage' => 20,
84 'gateway_configs' => [],
85 'gateway_order' => [],
86
87 // Discount Stacking Mode — controls how the Advanced Discount and
88 // Dynamic Pricing modules combine when both can fire on the same
89 // booking. Default 'both' preserves the legacy stacked behavior
90 // (discount on top of DP-adjusted price). The Settings → Pricing
91 // tab only surfaces this setting when BOTH modules are enabled,
92 // and CalculationService only enforces a non-default mode when
93 // BOTH modules are loaded — so sites with only one (or neither)
94 // module see zero behavior change.
95 //
96 // Allowed: 'both' | 'discount_only' | 'dynamic_pricing_only' | 'best_for_customer'
97 'discount_stacking_mode' => 'both',
98
99 // Scheduled/Recurring Payment Settings (Pro feature - defaults disabled)
100 'enable_scheduled_payments' => false,
101 'scheduled_payment_type' => 'single', // single, installments
102 'scheduled_payment_days' => 15, // Days until first scheduled payment
103 'scheduled_payment_installments' => 1, // Number of installments (if type is installments)
104 'scheduled_payment_interval' => 30, // Days between installments
105 'scheduled_payment_reminder_days' => 3, // Days before to send reminder
106 'allow_save_payment_methods' => false,
107
108 // Email Settings (WordPress site defaults when Yatra options are missing)
109 'admin_email' => $wpAdminEmail,
110 'from_email' => $wpAdminEmail,
111 'from_name' => $wpSiteName,
112 'email_template_booking' => true,
113 'email_template_confirmation' => true,
114 'email_template_cancellation' => true,
115 'email_template_reminder' => true,
116 'email_template_admin_new_booking' => true,
117 'email_template_admin_payment' => true,
118 'email_template_admin_cancellation' => true,
119 'email_template_trip_consent' => true,
120 'email_template_customer_verification' => true,
121 'email_template_guest_verification' => true,
122 'email_template_booking_completed' => true,
123 'email_template_booking_expired_customer' => true,
124 'email_template_admin_booking_expired' => true,
125 'email_template_scheduled_payment_reminder' => true,
126 'email_template_scheduled_payment_succeeded' => true,
127 'email_template_scheduled_payment_failed' => true,
128 'email_template_admin_scheduled_payment_failed' => true,
129 'email_template_enquiry_received' => true,
130 'email_template_enquiry_admin' => true,
131 'email_template_enquiry_response' => true,
132 'email_template_review_request' => true,
133 'email_template_abandoned_booking_recovery_first' => true,
134 'email_template_abandoned_booking_recovery_second' => true,
135 'email_template_abandoned_booking_recovery_final' => true,
136 'smtp_enabled' => false,
137 'smtp_host' => 'smtp.gmail.com',
138 'smtp_port' => 587,
139 'smtp_username' => '',
140 'smtp_password' => '',
141 'smtp_encryption' => 'tls',
142
143 // Customer Settings
144 'customer_registration' => true,
145 'customer_fields' => [],
146 'require_email_verification' => false,
147 // Per-booking verification for guest checkouts. Distinct from the
148 // account-creation `require_email_verification` flag because a guest
149 // never registers — the verification is gated on the booking itself
150 // (BookingSessionController checks this when admitting a guest).
151 'require_guest_email_verification' => false,
152 'customer_account_page' => '',
153 'allow_customer_reviews' => true,
154 'customer_dashboard_enabled' => true,
155
156 // Review Settings
157 'enable_reviews' => true,
158 'require_booking' => true,
159 'auto_approve_reviews' => false,
160 'review_moderation' => true,
161 'min_rating' => 1,
162 'allow_anonymous_reviews' => false,
163 'review_reminder_days' => 7,
164
165 // Tax Settings
166 'enable_tax' => false,
167 'tax_name' => __('Tax', 'yatra'),
168 'tax_rate' => 0,
169 'tax_inclusive' => false,
170 'vat_number' => '',
171 'tax_by_country' => false,
172 'tax_rates' => [],
173 'multiple_taxes_enabled' => false,
174 'multiple_taxes' => [],
175 'multiple_taxes_by_country' => [],
176
177 // Currency Settings
178 'default_currency' => 'USD',
179 'multi_currency' => false,
180 'currency_position' => 'left',
181 'currency_decimals' => 2,
182 'decimal_separator'=>'.',
183 'thousand_separator'=>',',
184
185 // Notification Settings (SMS / future channels — booking email toggles live under Email → Templates)
186 'sms_notifications' => false,
187 'sms_provider' => '',
188 'sms_api_key' => '',
189
190 // Integration Settings
191 'google_analytics' => '',
192 'facebook_pixel' => '',
193 'recaptcha_enabled' => false,
194 'recaptcha_site_key' => '',
195 'recaptcha_secret_key' => '',
196
197 // Permalink Settings
198 'trip_base' => 'trip',
199 'destination_base' => 'destination',
200 'activity_base' => 'activity',
201 'trip_category_base' => 'trip-category',
202 'booking_base' => 'book',
203 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
204 'enable_wishlist' => false,
205
206 // Search & Listing storefront UX. Defaults preserve current behaviour:
207 // every search field shown (true) and mobile filters expanded (false),
208 // so existing installs are unchanged until the owner opts in. Booleans
209 // are auto-sanitized from the default type.
210 'search_show_keyword' => true,
211 'search_show_destination' => true,
212 'search_show_activities' => true,
213 'search_show_duration' => true,
214 'search_show_budget' => true,
215 'collapse_filters_on_mobile' => false,
216
217 // Booking Page Settings
218 'use_booking_page' => false,
219 'booking_page_id' => 0,
220
221 // Legal Pages (Booking UI)
222 'terms_page_id' => 0,
223 'privacy_policy_page_id' => 0,
224
225 // SEO Settings
226 'seo_trip_meta_title' => '',
227 'seo_trip_meta_description' => '',
228 'seo_trip_meta_keywords' => '',
229 'seo_trip_meta_image' => 0,
230
231 // Advanced Settings
232 'debug_mode' => false,
233 'enable_logging' => false,
234 'cache_enabled' => true,
235 'api_key' => '',
236 'api_rate_limit' => 100,
237 'session_timeout' => 3600,
238
239 // Booking Form Builder
240 'booking_form_config' => [],
241 ];
242
243 $base_settings = array_merge(
244 $base_settings,
245 \Yatra\Services\EmailTemplateDefaults::settingsOptionDefaults()
246 );
247
248 // Allow Pro plugins to add their settings via filter
249 $this->default_settings = apply_filters('yatra_settings_default_fields', $base_settings);
250 }
251
252 public function register_routes(): void
253 {
254 $namespace = 'yatra/v1';
255 $base = 'settings';
256
257 register_rest_route($namespace, '/' . $base, [
258 [
259 'methods' => \WP_REST_Server::READABLE,
260 'callback' => [$this, 'get_settings'],
261 'permission_callback' => [$this, 'check_permission'],
262 ],
263 [
264 'methods' => \WP_REST_Server::EDITABLE,
265 'callback' => [$this, 'update_settings'],
266 'permission_callback' => [$this, 'check_permission'],
267 ],
268 ]);
269
270 // Flush rewrite rules endpoint
271 register_rest_route($namespace, '/' . $base . '/flush-rewrite-rules', [
272 [
273 'methods' => \WP_REST_Server::CREATABLE,
274 'callback' => [$this, 'flush_rewrite_rules'],
275 'permission_callback' => [$this, 'check_permission'],
276 ],
277 ]);
278
279 // Get WordPress pages for booking page selection
280 register_rest_route($namespace, '/' . $base . '/pages', [
281 [
282 'methods' => \WP_REST_Server::READABLE,
283 'callback' => [$this, 'get_pages'],
284 'permission_callback' => [$this, 'check_permission'],
285 ],
286 ]);
287
288 // Check if page has booking shortcode
289 register_rest_route($namespace, '/' . $base . '/check-shortcode/(?P<page_id>\d+)', [
290 [
291 'methods' => \WP_REST_Server::READABLE,
292 'callback' => [$this, 'check_booking_shortcode'],
293 'permission_callback' => [$this, 'check_permission'],
294 ],
295 ]);
296
297 // Insert booking shortcode into page
298 register_rest_route($namespace, '/' . $base . '/insert-shortcode/(?P<page_id>\d+)', [
299 [
300 'methods' => \WP_REST_Server::CREATABLE,
301 'callback' => [$this, 'insert_booking_shortcode'],
302 'permission_callback' => [$this, 'check_permission'],
303 ],
304 ]);
305
306 register_rest_route($namespace, '/' . $base . '/email-template-preview', [
307 [
308 'methods' => \WP_REST_Server::CREATABLE,
309 'callback' => [$this, 'preview_core_email_template'],
310 'permission_callback' => [$this, 'check_permission'],
311 ],
312 ]);
313 }
314
315 /**
316 * Preview a core (settings-backed) transactional template with sample merge data.
317 */
318 public function preview_core_email_template(WP_REST_Request $request)
319 {
320 try {
321 $params = $request->get_json_params();
322 if (!is_array($params)) {
323 return $this->error_response(__('Invalid request body.', 'yatra'), 400);
324 }
325
326 $templateKey = sanitize_key($params['template_key'] ?? '');
327 $subjectTpl = sanitize_text_field($params['subject'] ?? '');
328 $bodyTpl = wp_kses_post($params['body'] ?? '');
329 $tripId = isset($params['trip_id']) ? (int) $params['trip_id'] : 0;
330 $tripId = $tripId > 0 ? $tripId : null;
331
332 $rendered = EmailTemplatePreviewService::render($templateKey, $subjectTpl, $bodyTpl, $tripId);
333
334 return $this->success_response([
335 'success' => true,
336 'data' => [
337 'subject' => $rendered['subject'],
338 'body' => $rendered['body'],
339 ],
340 ]);
341 } catch (\InvalidArgumentException $e) {
342 return $this->error_response($e->getMessage(), 400);
343 } catch (\Exception $e) {
344 return $this->error_response($e->getMessage(), 500);
345 }
346 }
347
348 /**
349 * Plugin settings — high-sensitivity cap. By default only the
350 * Owner role holds `yatra_manage_settings` (Manager doesn't, by
351 * design — settings include payment gateway routing, email
352 * delivery configuration, currency formatting and similar
353 * global behaviour). WP admins pass via the Team module's
354 * admin-fallback filter.
355 */
356 public function check_permission(?WP_REST_Request $request = null): bool
357 {
358 if (!is_user_logged_in()) {
359 return false;
360 }
361 return current_user_can('yatra_manage_settings');
362 }
363
364 /**
365 * Get all settings
366 */
367 public function get_settings(WP_REST_Request $request)
368 {
369 try {
370 $settings = [];
371
372 // Get all settings from WordPress options table with yatra_ prefix
373 foreach ($this->default_settings as $key => $default_value) {
374 $option_name = 'yatra_' . $key;
375 $value = get_option($option_name, false);
376
377 // Only use default if option doesn't exist (wasn't set by InstallerService)
378 if ($value === false) {
379 $value = $default_value;
380 }
381
382 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
383 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
384 $wp = (string) get_option('admin_email', '');
385 $value = $wp !== '' ? $wp : $value;
386 }
387 if ($key === 'from_name' && is_string($value) && trim($value) === '') {
388 $wp = (string) get_bloginfo('name');
389 $value = $wp !== '' ? $wp : $value;
390 }
391
392 // Handle serialized arrays (for fields like payment_gateways, customer_fields, etc.)
393 if (is_string($value) && is_serialized($value)) {
394 $value = maybe_unserialize($value);
395 }
396
397 // Ensure arrays are returned as arrays (not objects)
398 if (is_array($default_value) && !is_array($value)) {
399 $value = [];
400 }
401
402 $settings[$key] = $value;
403 }
404
405 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
406 $settings['booking_form_config'] = \Yatra\Services\SettingsService::getBookingFormConfig();
407
408 // Merge in flexible payment settings from Pro module if enabled
409 $flexible_payment_settings = apply_filters('yatra_get_flexible_payment_settings', []);
410 if (!empty($flexible_payment_settings)) {
411 $settings = array_merge($settings, $flexible_payment_settings);
412 }
413
414 $scheduled_payment_settings = apply_filters('yatra_get_scheduled_payment_settings', []);
415 if (!empty($scheduled_payment_settings)) {
416 $settings = array_merge($settings, $scheduled_payment_settings);
417 }
418
419 // Scheduled payment keys are owned by Pro (yatra_pro_scheduled_payments), not yatra_* options.
420 foreach (
421 [
422 'enable_scheduled_payments',
423 'scheduled_payment_type',
424 'scheduled_payment_days',
425 'scheduled_payment_installments',
426 'scheduled_payment_interval',
427 'scheduled_payment_reminder_days',
428 ] as $sk
429 ) {
430 if (array_key_exists($sk, $this->default_settings)) {
431 $settings[$sk] = \Yatra\Services\SettingsService::get(
432 $sk,
433 $this->default_settings[$sk]
434 );
435 }
436 }
437
438 $settings = $this->syncAccountRouteSettingsForResponse($settings);
439
440 /**
441 * Allow Pro modules to align REST payloads with canonical option stores
442 * (e.g. GA4 settings that also live in yatra_google_analytics_settings).
443 */
444 $settings = apply_filters('yatra_rest_settings', $settings);
445
446 return $this->success_response($settings);
447 } catch (\Exception $e) {
448 return $this->error_response($e->getMessage(), 500);
449 }
450 }
451
452 /**
453 * Update settings
454 */
455 public function update_settings(WP_REST_Request $request)
456 {
457 try {
458 $data = $request->get_json_params();
459
460 if (!is_array($data)) {
461 return $this->error_response('Invalid settings data', 400);
462 }
463
464 $updated = [];
465 $errors = [];
466
467 // Check if Dynamic Form Field module is enabled
468 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
469
470 // Check if Flexible Payments module is enabled (Pro feature)
471 $is_flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
472
473 $is_scheduled_payments_module = apply_filters('yatra_scheduled_payments_module_active', false);
474
475 // Flexible payment settings keys (Pro only)
476 $flexible_payment_keys = [
477 'deposit_required', 'deposit_percentage', 'partial_payment',
478 'partial_payment_percentage', 'enable_deposit', 'allow_save_payment_methods',
479 ];
480
481 $scheduled_payment_keys = [
482 'enable_scheduled_payments',
483 'scheduled_payment_type',
484 'scheduled_payment_days',
485 'scheduled_payment_installments',
486 'scheduled_payment_interval',
487 'scheduled_payment_reminder_days',
488 ];
489
490 // Collect flexible payment settings to delegate to Pro
491 $flexible_payment_settings = [];
492
493 $scheduled_payment_settings_batch = [];
494
495 // Process each setting
496 foreach ($data as $key => $value) {
497 // Skip booking_form_config if Dynamic Form Field module is not enabled
498 // This allows the settings to save without error when the module is disabled
499 if ($key === 'booking_form_config' && !$is_dynamic_form_enabled) {
500 continue;
501 }
502
503 // Delegate flexible payment settings to Pro module
504 if (in_array($key, $flexible_payment_keys, true)) {
505 if ($is_flexible_payments_enabled) {
506 $flexible_payment_settings[$key] = $value;
507 }
508 // Skip saving in Free plugin - Pro handles these
509 continue;
510 }
511
512 if (in_array($key, $scheduled_payment_keys, true)) {
513 if ($is_scheduled_payments_module) {
514 $scheduled_payment_settings_batch[$key] = $value;
515 }
516 continue;
517 }
518
519 // Wishlist toggle: only meaningful with Yatra Pro active
520 if ($key === 'enable_wishlist' && !apply_filters('yatra_is_pro_active', false)) {
521 continue;
522 }
523
524 // Validate that the key exists in default settings
525 if (!array_key_exists($key, $this->default_settings)) {
526 $errors[] = sprintf('Unknown setting: %s', $key);
527 continue;
528 }
529
530 // Sanitize and validate the value based on its type
531 $sanitized_value = $this->sanitize_setting($key, $value);
532
533 if ($sanitized_value === null) {
534 $errors[] = sprintf('Invalid value for setting: %s', $key);
535 continue;
536 }
537
538 // Save to WordPress options table with yatra_ prefix
539 $option_name = 'yatra_' . $key;
540
541 // Serialize arrays for storage
542 if (is_array($sanitized_value)) {
543 $sanitized_value = maybe_serialize($sanitized_value);
544 }
545
546 $result = update_option($option_name, $sanitized_value);
547
548 if ($result !== false) {
549 $updated[] = $key;
550 }
551 }
552
553 // Delegate flexible payment settings to Pro module for saving
554 if (!empty($flexible_payment_settings) && $is_flexible_payments_enabled) {
555 do_action('yatra_save_flexible_payment_settings', $flexible_payment_settings);
556 $updated = array_merge($updated, array_keys($flexible_payment_settings));
557 }
558
559 if (!empty($scheduled_payment_settings_batch) && $is_scheduled_payments_module) {
560 do_action('yatra_save_scheduled_payment_settings', $scheduled_payment_settings_batch);
561 $updated = array_merge($updated, array_keys($scheduled_payment_settings_batch));
562 }
563
564 // Sync currency keys: keep 'currency' and 'default_currency' in sync
565 // Admin UI has both Payment Settings (currency) and Currency Settings (default_currency)
566 if (in_array('default_currency', $updated, true) && !in_array('currency', $updated, true)) {
567 $sync_currency = get_option('yatra_default_currency', 'USD');
568 update_option('yatra_currency', $sync_currency);
569 } elseif (in_array('currency', $updated, true) && !in_array('default_currency', $updated, true)) {
570 $sync_currency = get_option('yatra_currency', 'USD');
571 update_option('yatra_default_currency', $sync_currency);
572 }
573
574 if (in_array('customer_account_page', $updated, true)) {
575 $this->persistAccountBaseFromCustomerAccountPage();
576 }
577
578 if (!empty($errors)) {
579 $errorSummary = implode('; ', $errors);
580 return $this->error_response(
581 sprintf('Some settings could not be updated: %s', $errorSummary),
582 400,
583 [
584 'errors' => $errors,
585 'updated' => $updated,
586 ]
587 );
588 }
589
590 // Flush rewrite rules if permalink settings were updated
591 if (in_array('trip_base', $updated, true) ||
592 in_array('destination_base', $updated, true) ||
593 in_array('activity_base', $updated, true) ||
594 in_array('trip_category_base', $updated, true) ||
595 in_array('booking_base', $updated, true) ||
596 in_array('use_booking_page', $updated, true) ||
597 in_array('booking_page_id', $updated, true) ||
598 in_array('customer_account_page', $updated, true)) {
599 // Use hard flush to ensure rules are saved to database
600 flush_rewrite_rules(true);
601 }
602
603 if (!empty($updated)) {
604 \Yatra\Services\SettingsService::reload();
605 }
606
607 // Cross-validation: booking-auth settings interact via OR
608 // logic in booking-content.php, so some combinations are
609 // semantically inconsistent or redundant. We don't block
610 // the save (the resulting state still has well-defined
611 // behavior), but we surface a clear notice so the operator
612 // understands what they just configured.
613 //
614 // require_login=true + allow_guest_checkout=true →
615 // require_login wins; allow_guest_checkout is a no-op.
616 // require_login=true + allow_guest_checkout=false →
617 // Strictest setting (login required, no guest path).
618 // Internally consistent.
619 // require_login=false + allow_guest_checkout=false →
620 // Guests blocked, logged-in users can book. Consistent.
621 // require_login=false + allow_guest_checkout=true →
622 // Default. Permissive.
623 $notices = [];
624 $effective_require_login = \array_key_exists('require_login', $data)
625 ? (bool) $data['require_login']
626 : (bool) \Yatra\Services\SettingsService::get('require_login', false);
627 $effective_allow_guest = \array_key_exists('allow_guest_checkout', $data)
628 ? (bool) $data['allow_guest_checkout']
629 : (bool) \Yatra\Services\SettingsService::get('allow_guest_checkout', true);
630
631 if ($effective_require_login && $effective_allow_guest) {
632 $notices[] = [
633 'level' => 'warning',
634 'code' => 'booking_auth_redundant',
635 'message' => __(
636 'Heads up: "Require login" is on, so "Allow guest checkout" has no effect — every customer will need to log in to book. To accept guests, turn "Require login" off.',
637 'yatra'
638 ),
639 ];
640 }
641
642 // Scheduled Payments + guest checkout — incompatible at
643 // the gateway level. Scheduled charges require a saved
644 // payment-method tied to a customer record on the
645 // gateway side (Stripe Customer, etc.), which in turn
646 // requires a logged-in WP user. When both settings are
647 // on, the system gracefully skips installment creation
648 // for guest bookings — but operators expect them to
649 // work and only discover the gap when reconciling
650 // unpaid bookings weeks later. Surface this proactively.
651 $effective_scheduled_payments = \array_key_exists('enable_scheduled_payments', $data)
652 ? (bool) $data['enable_scheduled_payments']
653 : (bool) \Yatra\Services\SettingsService::get('enable_scheduled_payments', false);
654 if (
655 $effective_scheduled_payments
656 && $effective_allow_guest
657 && !$effective_require_login
658 ) {
659 $notices[] = [
660 'level' => 'info',
661 'code' => 'scheduled_payments_guest_caveat',
662 'message' => __(
663 'Scheduled Payments is on with guest checkout allowed. Scheduled installments only run for bookings made by logged-in customers (they need a saved payment method tied to their account). Guest bookings will be charged in full at checkout instead. Turn on "Require login" if every booking must support installments.',
664 'yatra'
665 ),
666 ];
667 }
668
669 $response = [
670 'message' => 'Settings updated successfully',
671 'updated' => $updated,
672 ];
673 if ($notices !== []) {
674 $response['notices'] = $notices;
675 }
676 return $this->success_response($response);
677 } catch (\Exception $e) {
678 return $this->error_response($e->getMessage(), 500);
679 }
680 }
681
682 /**
683 * Sanitize and validate setting value
684 * Pro plugins can handle sanitization of their own settings via filter
685 *
686 * @param mixed $value
687 * @return mixed
688 */
689 private function sanitize_setting(string $key, $value)
690 {
691 $default = $this->default_settings[$key] ?? null;
692 $default_type = gettype($default);
693
694 // Allow Pro plugins to handle sanitization of their own settings
695 $filtered_value = apply_filters('yatra_sanitize_setting', null, $key, $value, $default);
696 if ($filtered_value !== null) {
697 return $filtered_value;
698 }
699
700 // Handle null values - use default
701 if ($value === null) {
702 return $default;
703 }
704
705 // Handle arrays
706 if (is_array($default)) {
707 if (!is_array($value)) {
708 return null;
709 }
710 // Sanitize array values
711 return array_map(function($item) {
712 if (is_string($item)) {
713 return sanitize_text_field($item);
714 }
715 if (is_numeric($item)) {
716 return is_float($item) ? (float) $item : (int) $item;
717 }
718 if (is_bool($item)) {
719 return (bool) $item;
720 }
721 if (is_array($item)) {
722 return $this->sanitize_array($item);
723 }
724 return $item;
725 }, $value);
726 }
727
728 // Handle booleans (REST may send true/false strings)
729 if (is_bool($default)) {
730 if (is_bool($value)) {
731 return $value;
732 }
733 if (is_string($value)) {
734 $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
735 return $parsed !== null ? $parsed : (bool) $value;
736 }
737 return (bool) $value;
738 }
739
740 // Handle integers
741 if (is_int($default)) {
742 if (!is_numeric($value)) {
743 return null;
744 }
745 $int_value = (int) $value;
746 // Validate ranges for specific fields
747 if ($key === 'booking_expiry_hours' && $int_value < 0) {
748 return null;
749 }
750 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
751 return null;
752 }
753 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
754 return null;
755 }
756 if ($key === 'tax_rate' && ($int_value < 0 || $int_value > 100)) {
757 return null;
758 }
759 if ($key === 'smtp_port' && ($int_value < 1 || $int_value > 65535)) {
760 return null;
761 }
762 return $int_value;
763 }
764
765 // Handle floats
766 if (is_float($default)) {
767 if (!is_numeric($value)) {
768 return null;
769 }
770 $float_value = (float) $value;
771 if ($float_value < 0) {
772 return null;
773 }
774 return $float_value;
775 }
776
777 // Handle strings
778 if (is_string($default)) {
779 if ($key === 'timezone') {
780 $tz = is_string($value) ? trim($value) : '';
781 if ($tz === '') {
782 return is_string($default) ? $default : 'UTC';
783 }
784 try {
785 new \DateTimeZone($tz);
786
787 return $tz;
788 } catch (\Exception $e) {
789 return is_string($default) ? $default : 'UTC';
790 }
791 }
792 if ($key === 'currency_position') {
793 $allowed = ['left', 'right', 'left_space', 'right_space', 'before', 'after'];
794 $v = is_string($value) ? strtolower(trim($value)) : '';
795
796 return in_array($v, $allowed, true) ? $v : (is_string($default) ? $default : 'left');
797 }
798 if ($key === 'discount_stacking_mode') {
799 // Strict enum — any other value silently falls back to the
800 // backward-compatible default so a malformed POST cannot
801 // change pricing behavior unexpectedly.
802 $allowed = ['both', 'discount_only', 'dynamic_pricing_only', 'best_for_customer'];
803 $v = is_string($value) ? strtolower(trim($value)) : '';
804
805 return in_array($v, $allowed, true) ? $v : 'both';
806 }
807 // Special handling for specific fields
808 if ($key === 'company_email' || $key === 'admin_email' || $key === 'from_email' || $key === 'smtp_username') {
809 return sanitize_email($value);
810 }
811 if ($key === 'company_website' || $key === 'company_logo' || $key === 'google_analytics' || $key === 'facebook_pixel') {
812 return esc_url_raw($value);
813 }
814 if ($key === 'seo_trip_meta_title') {
815 // Allow more characters for meta title, but strip HTML
816 return wp_strip_all_tags($value);
817 }
818 if ($key === 'seo_trip_meta_description') {
819 // Allow more characters for meta description, but strip HTML
820 return wp_strip_all_tags($value);
821 }
822 if ($key === 'seo_trip_meta_keywords') {
823 // Allow keywords, strip HTML and sanitize
824 return sanitize_text_field($value);
825 }
826 if ($key === 'frontend_primary_color') {
827 return \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor(is_string($value) ? $value : '');
828 }
829 if ($key === 'frontend_container_max_width') {
830 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
831 is_string($value) ? $value : ''
832 );
833 }
834 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
835 return wp_kses_post((string) $value);
836 }
837 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
838 return sanitize_text_field((string) $value);
839 }
840 if ($key === 'smtp_password' || $key === 'api_key' || $key === 'sms_api_key' || $key === 'recaptcha_secret_key') {
841 // Don't sanitize passwords/keys too aggressively
842 return sanitize_text_field($value);
843 }
844 if ($key === 'gateway_configs') {
845 // Handle nested array structure for gateway configs
846 if (is_array($value)) {
847 return $this->sanitize_gateway_configs($value);
848 }
849 return [];
850 }
851 if ($key === 'booking_form_config') {
852 // Handle nested array structure for booking form config
853 if (is_array($value)) {
854 return $this->sanitize_booking_form_config($value);
855 }
856 return [];
857 }
858 if ($key === 'tax_rates') {
859 // Handle nested array structure for tax rates
860 if (is_array($value)) {
861 return $this->sanitize_tax_rates($value);
862 }
863 return [];
864 }
865 return sanitize_text_field($value);
866 }
867
868 return $value;
869 }
870
871 /**
872 * Sanitize nested array
873 */
874 private function sanitize_array(array $array): array
875 {
876 $sanitized = [];
877 foreach ($array as $k => $v) {
878 $sanitized_key = is_string($k) ? sanitize_key($k) : $k;
879 if (is_array($v)) {
880 $sanitized[$sanitized_key] = $this->sanitize_array($v);
881 } elseif (is_string($v)) {
882 $sanitized[$sanitized_key] = sanitize_text_field($v);
883 } elseif (is_numeric($v)) {
884 $sanitized[$sanitized_key] = is_float($v) ? (float) $v : (int) $v;
885 } elseif (is_bool($v)) {
886 $sanitized[$sanitized_key] = (bool) $v;
887 } else {
888 $sanitized[$sanitized_key] = $v;
889 }
890 }
891 return $sanitized;
892 }
893
894 /**
895 * Sanitize gateway configs
896 */
897 private function sanitize_gateway_configs(array $configs): array
898 {
899 $sanitized = [];
900 foreach ($configs as $gateway => $config) {
901 if (!is_array($config)) {
902 continue;
903 }
904 $sanitized_gateway = sanitize_key($gateway);
905 $row = [
906 'enabled' => isset($config['enabled']) ? (bool) $config['enabled'] : false,
907 'icon' => isset($config['icon']) ? esc_url_raw($config['icon']) : '',
908 'title' => isset($config['title']) ? sanitize_text_field($config['title']) : '',
909 'description' => isset($config['description']) ? sanitize_textarea_field($config['description']) : '',
910 'api_key' => isset($config['api_key']) ? sanitize_text_field($config['api_key']) : '',
911 'api_secret' => isset($config['api_secret']) ? sanitize_text_field($config['api_secret']) : '',
912 'client_id' => isset($config['client_id']) ? sanitize_text_field($config['client_id']) : '',
913 'client_secret' => isset($config['client_secret']) ? sanitize_text_field($config['client_secret']) : '',
914 'merchant_id' => isset($config['merchant_id']) ? sanitize_text_field($config['merchant_id']) : '',
915 'public_key' => isset($config['public_key']) ? sanitize_text_field($config['public_key']) : '',
916 'private_key' => isset($config['private_key']) ? sanitize_text_field($config['private_key']) : '',
917 'webhook_secret' => isset($config['webhook_secret']) ? sanitize_text_field($config['webhook_secret']) : '',
918 'test_mode' => isset($config['test_mode']) ? (bool) $config['test_mode'] : false,
919 'sandbox' => isset($config['sandbox']) ? (bool) $config['sandbox'] : false,
920 ];
921
922 if ($sanitized_gateway === 'paypal') {
923 $mode = isset($config['mode']) && in_array((string) $config['mode'], ['simple', 'advanced'], true)
924 ? (string) $config['mode']
925 : 'simple';
926 $row['email'] = isset($config['email']) ? sanitize_email((string) $config['email']) : '';
927 $row['mode'] = $mode;
928 }
929
930 if ($sanitized_gateway === 'pay_later') {
931 $row['payment_deadline_days'] = isset($config['payment_deadline_days'])
932 ? max(1, min(60, (int) $config['payment_deadline_days']))
933 : 7;
934 $row['auto_cancel_days'] = isset($config['auto_cancel_days'])
935 ? max(0, min(30, (int) $config['auto_cancel_days']))
936 : 3;
937 $row['require_deposit'] = isset($config['require_deposit']) ? (bool) $config['require_deposit'] : false;
938 $row['deposit_amount'] = isset($config['deposit_amount'])
939 ? max(1, min(50, (int) $config['deposit_amount']))
940 : 10;
941 $row['reminder_days'] = isset($config['reminder_days'])
942 ? sanitize_text_field((string) $config['reminder_days'])
943 : '7,3,1';
944 }
945
946 if ($sanitized_gateway === 'stripe') {
947 $allowedStripeMethods = ['card', 'google_pay', 'apple_pay'];
948 $methodsRaw = isset($config['enabled_methods']) ? (string) $config['enabled_methods'] : '';
949 if ($methodsRaw !== '') {
950 $parts = array_filter(array_map('trim', explode(',', $methodsRaw)));
951 $normalized = [];
952 foreach ($parts as $part) {
953 $slug = strtolower($part);
954 if (in_array($slug, $allowedStripeMethods, true)) {
955 $normalized[] = $slug;
956 }
957 }
958 $row['enabled_methods'] = $normalized !== [] ? implode(',', $normalized) : 'card,google_pay,apple_pay';
959 } else {
960 $row['enabled_methods'] = 'card,google_pay,apple_pay';
961 }
962 foreach (['live_publishable_key', 'live_secret_key', 'test_publishable_key', 'test_secret_key'] as $stripeKey) {
963 if (array_key_exists($stripeKey, $config)) {
964 $row[$stripeKey] = sanitize_text_field((string) $config[$stripeKey]);
965 }
966 }
967 }
968
969 if ($sanitized_gateway === 'razorpay') {
970 $row['key_id'] = isset($config['key_id']) ? sanitize_text_field((string) $config['key_id']) : '';
971 $row['key_secret'] = isset($config['key_secret']) ? sanitize_text_field((string) $config['key_secret']) : '';
972 }
973
974 if ($sanitized_gateway === 'mollie') {
975 $row['api_key'] = isset($config['api_key']) ? sanitize_text_field((string) $config['api_key']) : '';
976 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
977 $allowedMollie = ['creditcard', 'ideal', 'bancontact', 'sofort', 'eps', 'giropay', 'paypal', 'sepadirectdebit'];
978 $row['payment_methods'] = $this->sanitizeGatewayStringList(
979 $config['payment_methods'] ?? [],
980 $allowedMollie,
981 ['creditcard', 'ideal', 'paypal']
982 );
983 }
984
985 if ($sanitized_gateway === 'paystack') {
986 $row['public_key'] = isset($config['public_key']) ? sanitize_text_field((string) $config['public_key']) : '';
987 $row['secret_key'] = isset($config['secret_key']) ? sanitize_text_field((string) $config['secret_key']) : '';
988 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
989 $allowedPaystack = ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'];
990 $row['payment_channels'] = $this->sanitizeGatewayStringList(
991 $config['payment_channels'] ?? [],
992 $allowedPaystack,
993 ['card', 'bank', 'ussd']
994 );
995 unset($row['private_key']);
996 }
997
998 if ($sanitized_gateway === 'square') {
999 $row['application_id'] = isset($config['application_id']) ? sanitize_text_field((string) $config['application_id']) : '';
1000 $row['access_token'] = isset($config['access_token']) ? sanitize_text_field((string) $config['access_token']) : '';
1001 $row['location_id'] = isset($config['location_id']) ? sanitize_text_field((string) $config['location_id']) : '';
1002 }
1003
1004 if ($sanitized_gateway === 'authorize_net') {
1005 $row['api_login_id'] = isset($config['api_login_id']) ? sanitize_text_field((string) $config['api_login_id']) : '';
1006 $row['transaction_key'] = isset($config['transaction_key']) ? sanitize_text_field((string) $config['transaction_key']) : '';
1007 $row['public_client_key'] = isset($config['public_client_key']) ? sanitize_text_field((string) $config['public_client_key']) : '';
1008 }
1009
1010 if ($sanitized_gateway === 'bank_transfer') {
1011 $row['bank_name'] = isset($config['bank_name']) ? sanitize_text_field((string) $config['bank_name']) : '';
1012 $row['account_name'] = isset($config['account_name']) ? sanitize_text_field((string) $config['account_name']) : '';
1013 $row['account_number'] = isset($config['account_number']) ? sanitize_text_field((string) $config['account_number']) : '';
1014 $row['routing_code'] = isset($config['routing_code']) ? sanitize_text_field((string) $config['routing_code']) : '';
1015 $row['instructions'] = isset($config['instructions']) ? sanitize_textarea_field((string) $config['instructions']) : '';
1016 }
1017
1018 /**
1019 * Allow Pro add-ons or custom code to append keys after core sanitization.
1020 *
1021 * @param array<string, mixed> $row
1022 * @param array<string, mixed> $config
1023 * @return array<string, mixed>
1024 */
1025 $row = apply_filters('yatra_sanitize_gateway_config_row', $row, $sanitized_gateway, $config);
1026
1027 $sanitized[$sanitized_gateway] = $row;
1028 }
1029 return $sanitized;
1030 }
1031
1032 /**
1033 * Normalize multiselect gateway options (Mollie methods, Paystack channels, etc.).
1034 *
1035 * @param mixed $input
1036 * @param array<int, string> $allowed
1037 * @param array<int, string> $default
1038 * @return array<int, string>
1039 */
1040 private function sanitizeGatewayStringList($input, array $allowed, array $default): array
1041 {
1042 if (is_string($input) && $input !== '') {
1043 $input = array_map('trim', explode(',', $input));
1044 }
1045 if (!is_array($input)) {
1046 return $default;
1047 }
1048 $out = [];
1049 foreach ($input as $v) {
1050 $slug = sanitize_key((string) $v);
1051 if ($slug !== '' && in_array($slug, $allowed, true)) {
1052 $out[] = $slug;
1053 }
1054 }
1055 $out = array_values(array_unique($out));
1056
1057 return $out !== [] ? $out : $default;
1058 }
1059
1060 /**
1061 * Sanitize tax rates
1062 */
1063 private function sanitize_tax_rates(array $rates): array
1064 {
1065 $sanitized = [];
1066 foreach ($rates as $country => $rate) {
1067 $sanitized_country = sanitize_text_field($country);
1068 if (is_numeric($rate)) {
1069 $float_rate = (float) $rate;
1070 if ($float_rate >= 0 && $float_rate <= 100) {
1071 $sanitized[$sanitized_country] = $float_rate;
1072 }
1073 }
1074 }
1075 return $sanitized;
1076 }
1077
1078 /**
1079 * Sanitize booking form configuration
1080 */
1081 private function sanitize_booking_form_config(array $config): array
1082 {
1083 $sanitized = [];
1084 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1085 $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1086 $allowed_widths = ['full', 'half', 'third'];
1087
1088 foreach ($config as $form_type => $form_config) {
1089 if (!in_array($form_type, $allowed_form_types, true)) {
1090 continue;
1091 }
1092
1093 $sanitized[$form_type] = [
1094 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1095 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1096 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1097 'fields' => [],
1098 ];
1099
1100 if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
1101 foreach ($form_config['fields'] as $field) {
1102 if (!is_array($field) || empty($field['id'])) {
1103 continue;
1104 }
1105
1106 $sanitized_field = [
1107 'id' => sanitize_key($field['id']),
1108 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1109 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1110 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1111 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1112 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1113 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1114 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? $field['width'] : 'full',
1115 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1116 ];
1117
1118 // Handle optional section
1119 if (!empty($field['section'])) {
1120 $sanitized_field['section'] = sanitize_key($field['section']);
1121 }
1122
1123 // Handle options for select fields
1124 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1125 $sanitized_field['options'] = [];
1126 foreach ($field['options'] as $option) {
1127 if (is_array($option) && isset($option['value'])) {
1128 $sanitized_field['options'][] = [
1129 'value' => sanitize_key($option['value']),
1130 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1131 ];
1132 }
1133 }
1134 }
1135
1136 // A text block is display-only content placed between fields:
1137 // keep its (safe-HTML) content, and it can never be required.
1138 if ($sanitized_field['type'] === 'text_block') {
1139 $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1140 $sanitized_field['required'] = false;
1141 }
1142
1143 $sanitized[$form_type]['fields'][] = $sanitized_field;
1144 }
1145
1146 // Sort fields by order
1147 usort($sanitized[$form_type]['fields'], function($a, $b) {
1148 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1149 });
1150 }
1151 }
1152
1153 return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1154 }
1155
1156 /**
1157 * Flush rewrite rules
1158 */
1159 public function flush_rewrite_rules(WP_REST_Request $request)
1160 {
1161 try {
1162 // Flush rewrite rules
1163 flush_rewrite_rules(true);
1164
1165 return $this->success_response([
1166 'message' => 'Rewrite rules flushed successfully',
1167 ]);
1168 } catch (\Exception $e) {
1169 return $this->error_response($e->getMessage(), 500);
1170 }
1171 }
1172
1173 /**
1174 * Get list of WordPress pages for booking page selection
1175 * Note: We don't check for shortcode here - it's checked on-demand when user selects a page
1176 */
1177 public function get_pages(WP_REST_Request $request)
1178 {
1179 try {
1180 $pages = get_pages([
1181 'post_status' => 'publish',
1182 'sort_column' => 'post_title',
1183 'sort_order' => 'ASC',
1184 ]);
1185
1186 $page_list = [];
1187 foreach ($pages as $page) {
1188 $page_list[] = [
1189 'id' => $page->ID,
1190 'title' => $page->post_title,
1191 'slug' => $page->post_name,
1192 'url' => get_permalink($page->ID),
1193 ];
1194 }
1195
1196 return $this->success_response($page_list);
1197 } catch (\Exception $e) {
1198 return $this->error_response($e->getMessage(), 500);
1199 }
1200 }
1201
1202 /**
1203 * Check if a page has the booking shortcode
1204 */
1205 public function check_booking_shortcode(WP_REST_Request $request)
1206 {
1207 try {
1208 $page_id = (int) $request->get_param('page_id');
1209
1210 if ($page_id <= 0) {
1211 return $this->error_response('Invalid page ID', 400);
1212 }
1213
1214 $page = get_post($page_id);
1215
1216 if (!$page || $page->post_type !== 'page') {
1217 return $this->error_response('Page not found', 404);
1218 }
1219
1220 $has_shortcode = has_shortcode($page->post_content, 'yatra_booking');
1221
1222 return $this->success_response([
1223 'page_id' => $page_id,
1224 'has_shortcode' => $has_shortcode,
1225 'page_title' => $page->post_title,
1226 'page_url' => get_permalink($page_id),
1227 'edit_url' => get_edit_post_link($page_id, 'raw'),
1228 ]);
1229 } catch (\Exception $e) {
1230 return $this->error_response($e->getMessage(), 500);
1231 }
1232 }
1233
1234 /**
1235 * Insert booking shortcode into a page
1236 */
1237 public function insert_booking_shortcode(WP_REST_Request $request)
1238 {
1239 try {
1240 $page_id = (int) $request->get_param('page_id');
1241
1242 if ($page_id <= 0) {
1243 return $this->error_response('Invalid page ID', 400);
1244 }
1245
1246 $page = get_post($page_id);
1247
1248 if (!$page || $page->post_type !== 'page') {
1249 return $this->error_response('Page not found', 404);
1250 }
1251
1252 // Check if shortcode already exists
1253 if (has_shortcode($page->post_content, 'yatra_booking')) {
1254 return $this->success_response([
1255 'message' => 'Shortcode already exists on this page',
1256 'page_id' => $page_id,
1257 'already_exists' => true,
1258 ]);
1259 }
1260
1261 // Append shortcode to page content
1262 $new_content = $page->post_content . "\n\n[yatra_booking]";
1263
1264 $result = wp_update_post([
1265 'ID' => $page_id,
1266 'post_content' => $new_content,
1267 ], true);
1268
1269 if (is_wp_error($result)) {
1270 return $this->error_response($result->get_error_message(), 500);
1271 }
1272
1273 return $this->success_response([
1274 'message' => 'Shortcode added successfully',
1275 'page_id' => $page_id,
1276 'page_url' => get_permalink($page_id),
1277 ]);
1278 } catch (\Exception $e) {
1279 return $this->error_response($e->getMessage(), 500);
1280 }
1281 }
1282
1283 /**
1284 * Keep Settings → Customer "account page" path aligned with {@see RouteMatcher} / {@see Router} (yatra_account_base).
1285 *
1286 * @param array<string, mixed> $settings
1287 * @return array<string, mixed>
1288 */
1289 private function syncAccountRouteSettingsForResponse(array $settings): array
1290 {
1291 // Prefer the full saved path so admin "View" matches Settings → Customer (not only yatra_account_base slug).
1292 $savedPath = get_option('yatra_customer_account_page', '');
1293 if (is_string($savedPath) && $savedPath !== '' && $savedPath !== '0') {
1294 $normalized = '/' . trim(str_replace('\\', '/', $savedPath), '/');
1295 if ($normalized === '/') {
1296 $normalized = '/my-account';
1297 }
1298 $settings['customer_account_page'] = $normalized;
1299
1300 return $settings;
1301 }
1302
1303 $stored = get_option('yatra_account_base', '');
1304 if (is_string($stored) && $stored !== '') {
1305 $settings['customer_account_page'] = '/' . $stored;
1306
1307 return $settings;
1308 }
1309
1310 $cpp = (string) ($settings['customer_account_page'] ?? '');
1311 $slug = self::accountSlugFromCustomerAccountPath($cpp !== '' ? $cpp : '/account');
1312 update_option('yatra_account_base', $slug);
1313 $settings['customer_account_page'] = '/' . $slug;
1314
1315 return $settings;
1316 }
1317
1318 private function persistAccountBaseFromCustomerAccountPage(): void
1319 {
1320 $cpp = (string) get_option('yatra_customer_account_page', '');
1321 update_option('yatra_account_base', self::accountSlugFromCustomerAccountPath($cpp));
1322 }
1323
1324 private static function accountSlugFromCustomerAccountPath(string $path): string
1325 {
1326 $path = trim(str_replace('\\', '/', $path), '/');
1327 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
1328 $segment = $parts !== [] ? end($parts) : 'account';
1329 $slug = sanitize_title($segment);
1330
1331 return $slug !== '' ? $slug : 'account';
1332 }
1333 }
1334
1335