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

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

1,346 lines 56.6 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 'enable_sitemap' => true,
231
232 // Advanced Settings
233 'debug_mode' => false,
234 'enable_logging' => false,
235 'cache_enabled' => true,
236 'api_key' => '',
237 'api_rate_limit' => 100,
238 'session_timeout' => 3600,
239
240 // Booking Form Builder
241 'booking_form_config' => [],
242 ];
243
244 $base_settings = array_merge(
245 $base_settings,
246 \Yatra\Services\EmailTemplateDefaults::settingsOptionDefaults()
247 );
248
249 // Allow Pro plugins to add their settings via filter
250 $this->default_settings = apply_filters('yatra_settings_default_fields', $base_settings);
251 }
252
253 public function register_routes(): void
254 {
255 $namespace = 'yatra/v1';
256 $base = 'settings';
257
258 register_rest_route($namespace, '/' . $base, [
259 [
260 'methods' => \WP_REST_Server::READABLE,
261 'callback' => [$this, 'get_settings'],
262 'permission_callback' => [$this, 'check_permission'],
263 ],
264 [
265 'methods' => \WP_REST_Server::EDITABLE,
266 'callback' => [$this, 'update_settings'],
267 'permission_callback' => [$this, 'check_permission'],
268 ],
269 ]);
270
271 // Flush rewrite rules endpoint
272 register_rest_route($namespace, '/' . $base . '/flush-rewrite-rules', [
273 [
274 'methods' => \WP_REST_Server::CREATABLE,
275 'callback' => [$this, 'flush_rewrite_rules'],
276 'permission_callback' => [$this, 'check_permission'],
277 ],
278 ]);
279
280 // Get WordPress pages for booking page selection
281 register_rest_route($namespace, '/' . $base . '/pages', [
282 [
283 'methods' => \WP_REST_Server::READABLE,
284 'callback' => [$this, 'get_pages'],
285 'permission_callback' => [$this, 'check_permission'],
286 ],
287 ]);
288
289 // Check if page has booking shortcode
290 register_rest_route($namespace, '/' . $base . '/check-shortcode/(?P<page_id>\d+)', [
291 [
292 'methods' => \WP_REST_Server::READABLE,
293 'callback' => [$this, 'check_booking_shortcode'],
294 'permission_callback' => [$this, 'check_permission'],
295 ],
296 ]);
297
298 // Insert booking shortcode into page
299 register_rest_route($namespace, '/' . $base . '/insert-shortcode/(?P<page_id>\d+)', [
300 [
301 'methods' => \WP_REST_Server::CREATABLE,
302 'callback' => [$this, 'insert_booking_shortcode'],
303 'permission_callback' => [$this, 'check_permission'],
304 ],
305 ]);
306
307 register_rest_route($namespace, '/' . $base . '/email-template-preview', [
308 [
309 'methods' => \WP_REST_Server::CREATABLE,
310 'callback' => [$this, 'preview_core_email_template'],
311 'permission_callback' => [$this, 'check_permission'],
312 ],
313 ]);
314 }
315
316 /**
317 * Preview a core (settings-backed) transactional template with sample merge data.
318 */
319 public function preview_core_email_template(WP_REST_Request $request)
320 {
321 try {
322 $params = $request->get_json_params();
323 if (!is_array($params)) {
324 return $this->error_response(__('Invalid request body.', 'yatra'), 400);
325 }
326
327 $templateKey = sanitize_key($params['template_key'] ?? '');
328 $subjectTpl = sanitize_text_field($params['subject'] ?? '');
329 $bodyTpl = wp_kses_post($params['body'] ?? '');
330 $tripId = isset($params['trip_id']) ? (int) $params['trip_id'] : 0;
331 $tripId = $tripId > 0 ? $tripId : null;
332
333 $rendered = EmailTemplatePreviewService::render($templateKey, $subjectTpl, $bodyTpl, $tripId);
334
335 return $this->success_response([
336 'success' => true,
337 'data' => [
338 'subject' => $rendered['subject'],
339 'body' => $rendered['body'],
340 ],
341 ]);
342 } catch (\InvalidArgumentException $e) {
343 return $this->error_response($e->getMessage(), 400);
344 } catch (\Exception $e) {
345 return $this->error_response($e->getMessage(), 500);
346 }
347 }
348
349 /**
350 * Plugin settings — high-sensitivity cap. By default only the
351 * Owner role holds `yatra_manage_settings` (Manager doesn't, by
352 * design — settings include payment gateway routing, email
353 * delivery configuration, currency formatting and similar
354 * global behaviour). WP admins pass via the Team module's
355 * admin-fallback filter.
356 */
357 public function check_permission(?WP_REST_Request $request = null): bool
358 {
359 if (!is_user_logged_in()) {
360 return false;
361 }
362 return current_user_can('yatra_manage_settings');
363 }
364
365 /**
366 * Get all settings
367 */
368 public function get_settings(WP_REST_Request $request)
369 {
370 try {
371 $settings = [];
372
373 // Get all settings from WordPress options table with yatra_ prefix
374 foreach ($this->default_settings as $key => $default_value) {
375 $option_name = 'yatra_' . $key;
376 $value = get_option($option_name, false);
377
378 // Only use default if option doesn't exist (wasn't set by InstallerService)
379 if ($value === false) {
380 $value = $default_value;
381 }
382
383 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
384 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
385 $wp = (string) get_option('admin_email', '');
386 $value = $wp !== '' ? $wp : $value;
387 }
388 if ($key === 'from_name' && is_string($value) && trim($value) === '') {
389 $wp = (string) get_bloginfo('name');
390 $value = $wp !== '' ? $wp : $value;
391 }
392
393 // Handle serialized arrays (for fields like payment_gateways, customer_fields, etc.)
394 if (is_string($value) && is_serialized($value)) {
395 $value = maybe_unserialize($value);
396 }
397
398 // Ensure arrays are returned as arrays (not objects)
399 if (is_array($default_value) && !is_array($value)) {
400 $value = [];
401 }
402
403 $settings[$key] = $value;
404 }
405
406 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
407 $settings['booking_form_config'] = \Yatra\Services\SettingsService::getBookingFormConfig();
408
409 // Merge in flexible payment settings from Pro module if enabled
410 $flexible_payment_settings = apply_filters('yatra_get_flexible_payment_settings', []);
411 if (!empty($flexible_payment_settings)) {
412 $settings = array_merge($settings, $flexible_payment_settings);
413 }
414
415 $scheduled_payment_settings = apply_filters('yatra_get_scheduled_payment_settings', []);
416 if (!empty($scheduled_payment_settings)) {
417 $settings = array_merge($settings, $scheduled_payment_settings);
418 }
419
420 // Scheduled payment keys are owned by Pro (yatra_pro_scheduled_payments), not yatra_* options.
421 foreach (
422 [
423 'enable_scheduled_payments',
424 'scheduled_payment_type',
425 'scheduled_payment_days',
426 'scheduled_payment_installments',
427 'scheduled_payment_interval',
428 'scheduled_payment_reminder_days',
429 ] as $sk
430 ) {
431 if (array_key_exists($sk, $this->default_settings)) {
432 $settings[$sk] = \Yatra\Services\SettingsService::get(
433 $sk,
434 $this->default_settings[$sk]
435 );
436 }
437 }
438
439 $settings = $this->syncAccountRouteSettingsForResponse($settings);
440
441 /**
442 * Allow Pro modules to align REST payloads with canonical option stores
443 * (e.g. GA4 settings that also live in yatra_google_analytics_settings).
444 */
445 $settings = apply_filters('yatra_rest_settings', $settings);
446
447 return $this->success_response($settings);
448 } catch (\Exception $e) {
449 return $this->error_response($e->getMessage(), 500);
450 }
451 }
452
453 /**
454 * Update settings
455 */
456 public function update_settings(WP_REST_Request $request)
457 {
458 try {
459 $data = $request->get_json_params();
460
461 if (!is_array($data)) {
462 return $this->error_response('Invalid settings data', 400);
463 }
464
465 $updated = [];
466 $errors = [];
467
468 // Check if Dynamic Form Field module is enabled
469 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
470
471 // Check if Flexible Payments module is enabled (Pro feature)
472 $is_flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
473
474 $is_scheduled_payments_module = apply_filters('yatra_scheduled_payments_module_active', false);
475
476 // Flexible payment settings keys (Pro only)
477 $flexible_payment_keys = [
478 'deposit_required', 'deposit_percentage', 'partial_payment',
479 'partial_payment_percentage', 'enable_deposit', 'allow_save_payment_methods',
480 ];
481
482 $scheduled_payment_keys = [
483 'enable_scheduled_payments',
484 'scheduled_payment_type',
485 'scheduled_payment_days',
486 'scheduled_payment_installments',
487 'scheduled_payment_interval',
488 'scheduled_payment_reminder_days',
489 ];
490
491 // Collect flexible payment settings to delegate to Pro
492 $flexible_payment_settings = [];
493
494 $scheduled_payment_settings_batch = [];
495
496 // Process each setting
497 foreach ($data as $key => $value) {
498 // Skip booking_form_config if Dynamic Form Field module is not enabled
499 // This allows the settings to save without error when the module is disabled
500 if ($key === 'booking_form_config' && !$is_dynamic_form_enabled) {
501 continue;
502 }
503
504 // Delegate flexible payment settings to Pro module
505 if (in_array($key, $flexible_payment_keys, true)) {
506 if ($is_flexible_payments_enabled) {
507 $flexible_payment_settings[$key] = $value;
508 }
509 // Skip saving in Free plugin - Pro handles these
510 continue;
511 }
512
513 if (in_array($key, $scheduled_payment_keys, true)) {
514 if ($is_scheduled_payments_module) {
515 $scheduled_payment_settings_batch[$key] = $value;
516 }
517 continue;
518 }
519
520 // Wishlist toggle: only meaningful with Yatra Pro active
521 if ($key === 'enable_wishlist' && !apply_filters('yatra_is_pro_active', false)) {
522 continue;
523 }
524
525 // Validate that the key exists in default settings
526 if (!array_key_exists($key, $this->default_settings)) {
527 $errors[] = sprintf('Unknown setting: %s', $key);
528 continue;
529 }
530
531 // Sanitize and validate the value based on its type
532 $sanitized_value = $this->sanitize_setting($key, $value);
533
534 if ($sanitized_value === null) {
535 $errors[] = sprintf('Invalid value for setting: %s', $key);
536 continue;
537 }
538
539 // Save to WordPress options table with yatra_ prefix
540 $option_name = 'yatra_' . $key;
541
542 // Serialize arrays for storage
543 if (is_array($sanitized_value)) {
544 $sanitized_value = maybe_serialize($sanitized_value);
545 }
546
547 $result = update_option($option_name, $sanitized_value);
548
549 if ($result !== false) {
550 $updated[] = $key;
551 }
552 }
553
554 // Delegate flexible payment settings to Pro module for saving
555 if (!empty($flexible_payment_settings) && $is_flexible_payments_enabled) {
556 do_action('yatra_save_flexible_payment_settings', $flexible_payment_settings);
557 $updated = array_merge($updated, array_keys($flexible_payment_settings));
558 }
559
560 if (!empty($scheduled_payment_settings_batch) && $is_scheduled_payments_module) {
561 do_action('yatra_save_scheduled_payment_settings', $scheduled_payment_settings_batch);
562 $updated = array_merge($updated, array_keys($scheduled_payment_settings_batch));
563 }
564
565 // Sync currency keys: keep 'currency' and 'default_currency' in sync
566 // Admin UI has both Payment Settings (currency) and Currency Settings (default_currency)
567 if (in_array('default_currency', $updated, true) && !in_array('currency', $updated, true)) {
568 $sync_currency = get_option('yatra_default_currency', 'USD');
569 update_option('yatra_currency', $sync_currency);
570 } elseif (in_array('currency', $updated, true) && !in_array('default_currency', $updated, true)) {
571 $sync_currency = get_option('yatra_currency', 'USD');
572 update_option('yatra_default_currency', $sync_currency);
573 }
574
575 if (in_array('customer_account_page', $updated, true)) {
576 $this->persistAccountBaseFromCustomerAccountPage();
577 }
578
579 if (!empty($errors)) {
580 $errorSummary = implode('; ', $errors);
581 return $this->error_response(
582 sprintf('Some settings could not be updated: %s', $errorSummary),
583 400,
584 [
585 'errors' => $errors,
586 'updated' => $updated,
587 ]
588 );
589 }
590
591 // Flush rewrite rules if permalink settings were updated
592 if (in_array('trip_base', $updated, true) ||
593 in_array('destination_base', $updated, true) ||
594 in_array('activity_base', $updated, true) ||
595 in_array('trip_category_base', $updated, true) ||
596 in_array('booking_base', $updated, true) ||
597 in_array('use_booking_page', $updated, true) ||
598 in_array('booking_page_id', $updated, true) ||
599 in_array('customer_account_page', $updated, true)) {
600 // Use hard flush to ensure rules are saved to database
601 flush_rewrite_rules(true);
602 }
603
604 if (!empty($updated)) {
605 \Yatra\Services\SettingsService::reload();
606 }
607
608 // Cross-validation: booking-auth settings interact via OR
609 // logic in booking-content.php, so some combinations are
610 // semantically inconsistent or redundant. We don't block
611 // the save (the resulting state still has well-defined
612 // behavior), but we surface a clear notice so the operator
613 // understands what they just configured.
614 //
615 // require_login=true + allow_guest_checkout=true →
616 // require_login wins; allow_guest_checkout is a no-op.
617 // require_login=true + allow_guest_checkout=false →
618 // Strictest setting (login required, no guest path).
619 // Internally consistent.
620 // require_login=false + allow_guest_checkout=false →
621 // Guests blocked, logged-in users can book. Consistent.
622 // require_login=false + allow_guest_checkout=true →
623 // Default. Permissive.
624 $notices = [];
625 $effective_require_login = \array_key_exists('require_login', $data)
626 ? (bool) $data['require_login']
627 : (bool) \Yatra\Services\SettingsService::get('require_login', false);
628 $effective_allow_guest = \array_key_exists('allow_guest_checkout', $data)
629 ? (bool) $data['allow_guest_checkout']
630 : (bool) \Yatra\Services\SettingsService::get('allow_guest_checkout', true);
631
632 if ($effective_require_login && $effective_allow_guest) {
633 $notices[] = [
634 'level' => 'warning',
635 'code' => 'booking_auth_redundant',
636 'message' => __(
637 '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.',
638 'yatra'
639 ),
640 ];
641 }
642
643 // Scheduled Payments + guest checkout — incompatible at
644 // the gateway level. Scheduled charges require a saved
645 // payment-method tied to a customer record on the
646 // gateway side (Stripe Customer, etc.), which in turn
647 // requires a logged-in WP user. When both settings are
648 // on, the system gracefully skips installment creation
649 // for guest bookings — but operators expect them to
650 // work and only discover the gap when reconciling
651 // unpaid bookings weeks later. Surface this proactively.
652 $effective_scheduled_payments = \array_key_exists('enable_scheduled_payments', $data)
653 ? (bool) $data['enable_scheduled_payments']
654 : (bool) \Yatra\Services\SettingsService::get('enable_scheduled_payments', false);
655 if (
656 $effective_scheduled_payments
657 && $effective_allow_guest
658 && !$effective_require_login
659 ) {
660 $notices[] = [
661 'level' => 'info',
662 'code' => 'scheduled_payments_guest_caveat',
663 'message' => __(
664 '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.',
665 'yatra'
666 ),
667 ];
668 }
669
670 $response = [
671 'message' => 'Settings updated successfully',
672 'updated' => $updated,
673 ];
674 if ($notices !== []) {
675 $response['notices'] = $notices;
676 }
677 return $this->success_response($response);
678 } catch (\Exception $e) {
679 return $this->error_response($e->getMessage(), 500);
680 }
681 }
682
683 /**
684 * Sanitize and validate setting value
685 * Pro plugins can handle sanitization of their own settings via filter
686 *
687 * @param mixed $value
688 * @return mixed
689 */
690 private function sanitize_setting(string $key, $value)
691 {
692 $default = $this->default_settings[$key] ?? null;
693 $default_type = gettype($default);
694
695 // Allow Pro plugins to handle sanitization of their own settings
696 $filtered_value = apply_filters('yatra_sanitize_setting', null, $key, $value, $default);
697 if ($filtered_value !== null) {
698 return $filtered_value;
699 }
700
701 // Handle null values - use default
702 if ($value === null) {
703 return $default;
704 }
705
706 // Handle arrays
707 if (is_array($default)) {
708 if (!is_array($value)) {
709 return null;
710 }
711 // Sanitize array values
712 return array_map(function($item) {
713 if (is_string($item)) {
714 return sanitize_text_field($item);
715 }
716 if (is_numeric($item)) {
717 return is_float($item) ? (float) $item : (int) $item;
718 }
719 if (is_bool($item)) {
720 return (bool) $item;
721 }
722 if (is_array($item)) {
723 return $this->sanitize_array($item);
724 }
725 return $item;
726 }, $value);
727 }
728
729 // Handle booleans (REST may send true/false strings)
730 if (is_bool($default)) {
731 if (is_bool($value)) {
732 return $value;
733 }
734 if (is_string($value)) {
735 $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
736 return $parsed !== null ? $parsed : (bool) $value;
737 }
738 return (bool) $value;
739 }
740
741 // Handle integers
742 if (is_int($default)) {
743 if (!is_numeric($value)) {
744 return null;
745 }
746 $int_value = (int) $value;
747 // Validate ranges for specific fields
748 if ($key === 'booking_expiry_hours' && $int_value < 0) {
749 return null;
750 }
751 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
752 return null;
753 }
754 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
755 return null;
756 }
757 if ($key === 'tax_rate' && ($int_value < 0 || $int_value > 100)) {
758 return null;
759 }
760 if ($key === 'smtp_port' && ($int_value < 1 || $int_value > 65535)) {
761 return null;
762 }
763 return $int_value;
764 }
765
766 // Handle floats
767 if (is_float($default)) {
768 if (!is_numeric($value)) {
769 return null;
770 }
771 $float_value = (float) $value;
772 if ($float_value < 0) {
773 return null;
774 }
775 return $float_value;
776 }
777
778 // Handle strings
779 if (is_string($default)) {
780 if ($key === 'timezone') {
781 $tz = is_string($value) ? trim($value) : '';
782 if ($tz === '') {
783 return is_string($default) ? $default : 'UTC';
784 }
785 try {
786 new \DateTimeZone($tz);
787
788 return $tz;
789 } catch (\Exception $e) {
790 return is_string($default) ? $default : 'UTC';
791 }
792 }
793 if ($key === 'currency_position') {
794 $allowed = ['left', 'right', 'left_space', 'right_space', 'before', 'after'];
795 $v = is_string($value) ? strtolower(trim($value)) : '';
796
797 return in_array($v, $allowed, true) ? $v : (is_string($default) ? $default : 'left');
798 }
799 if ($key === 'discount_stacking_mode') {
800 // Strict enum — any other value silently falls back to the
801 // backward-compatible default so a malformed POST cannot
802 // change pricing behavior unexpectedly.
803 $allowed = ['both', 'discount_only', 'dynamic_pricing_only', 'best_for_customer'];
804 $v = is_string($value) ? strtolower(trim($value)) : '';
805
806 return in_array($v, $allowed, true) ? $v : 'both';
807 }
808 // Special handling for specific fields
809 if ($key === 'company_email' || $key === 'admin_email' || $key === 'from_email' || $key === 'smtp_username') {
810 return sanitize_email($value);
811 }
812 if ($key === 'company_website' || $key === 'company_logo' || $key === 'google_analytics' || $key === 'facebook_pixel') {
813 return esc_url_raw($value);
814 }
815 if ($key === 'seo_trip_meta_title') {
816 // Allow more characters for meta title, but strip HTML
817 return wp_strip_all_tags($value);
818 }
819 if ($key === 'seo_trip_meta_description') {
820 // Allow more characters for meta description, but strip HTML
821 return wp_strip_all_tags($value);
822 }
823 if ($key === 'seo_trip_meta_keywords') {
824 // Allow keywords, strip HTML and sanitize
825 return sanitize_text_field($value);
826 }
827 if ($key === 'frontend_primary_color') {
828 return \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor(is_string($value) ? $value : '');
829 }
830 if ($key === 'frontend_container_max_width') {
831 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
832 is_string($value) ? $value : ''
833 );
834 }
835 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
836 return wp_kses_post((string) $value);
837 }
838 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
839 return sanitize_text_field((string) $value);
840 }
841 if ($key === 'smtp_password' || $key === 'api_key' || $key === 'sms_api_key' || $key === 'recaptcha_secret_key') {
842 // Don't sanitize passwords/keys too aggressively
843 return sanitize_text_field($value);
844 }
845 if ($key === 'gateway_configs') {
846 // Handle nested array structure for gateway configs
847 if (is_array($value)) {
848 return $this->sanitize_gateway_configs($value);
849 }
850 return [];
851 }
852 if ($key === 'booking_form_config') {
853 // Handle nested array structure for booking form config
854 if (is_array($value)) {
855 return $this->sanitize_booking_form_config($value);
856 }
857 return [];
858 }
859 if ($key === 'tax_rates') {
860 // Handle nested array structure for tax rates
861 if (is_array($value)) {
862 return $this->sanitize_tax_rates($value);
863 }
864 return [];
865 }
866 return sanitize_text_field($value);
867 }
868
869 return $value;
870 }
871
872 /**
873 * Sanitize nested array
874 */
875 private function sanitize_array(array $array): array
876 {
877 $sanitized = [];
878 foreach ($array as $k => $v) {
879 $sanitized_key = is_string($k) ? sanitize_key($k) : $k;
880 if (is_array($v)) {
881 $sanitized[$sanitized_key] = $this->sanitize_array($v);
882 } elseif (is_string($v)) {
883 $sanitized[$sanitized_key] = sanitize_text_field($v);
884 } elseif (is_numeric($v)) {
885 $sanitized[$sanitized_key] = is_float($v) ? (float) $v : (int) $v;
886 } elseif (is_bool($v)) {
887 $sanitized[$sanitized_key] = (bool) $v;
888 } else {
889 $sanitized[$sanitized_key] = $v;
890 }
891 }
892 return $sanitized;
893 }
894
895 /**
896 * Sanitize gateway configs
897 */
898 private function sanitize_gateway_configs(array $configs): array
899 {
900 $sanitized = [];
901 foreach ($configs as $gateway => $config) {
902 if (!is_array($config)) {
903 continue;
904 }
905 $sanitized_gateway = sanitize_key($gateway);
906 $row = [
907 'enabled' => isset($config['enabled']) ? (bool) $config['enabled'] : false,
908 'icon' => isset($config['icon']) ? esc_url_raw($config['icon']) : '',
909 'title' => isset($config['title']) ? sanitize_text_field($config['title']) : '',
910 'description' => isset($config['description']) ? sanitize_textarea_field($config['description']) : '',
911 'api_key' => isset($config['api_key']) ? sanitize_text_field($config['api_key']) : '',
912 'api_secret' => isset($config['api_secret']) ? sanitize_text_field($config['api_secret']) : '',
913 'client_id' => isset($config['client_id']) ? sanitize_text_field($config['client_id']) : '',
914 'client_secret' => isset($config['client_secret']) ? sanitize_text_field($config['client_secret']) : '',
915 'merchant_id' => isset($config['merchant_id']) ? sanitize_text_field($config['merchant_id']) : '',
916 'public_key' => isset($config['public_key']) ? sanitize_text_field($config['public_key']) : '',
917 'private_key' => isset($config['private_key']) ? sanitize_text_field($config['private_key']) : '',
918 'webhook_secret' => isset($config['webhook_secret']) ? sanitize_text_field($config['webhook_secret']) : '',
919 'test_mode' => isset($config['test_mode']) ? (bool) $config['test_mode'] : false,
920 'sandbox' => isset($config['sandbox']) ? (bool) $config['sandbox'] : false,
921 ];
922
923 if ($sanitized_gateway === 'paypal') {
924 $mode = isset($config['mode']) && in_array((string) $config['mode'], ['simple', 'advanced'], true)
925 ? (string) $config['mode']
926 : 'simple';
927 $row['email'] = isset($config['email']) ? sanitize_email((string) $config['email']) : '';
928 $row['mode'] = $mode;
929 }
930
931 if ($sanitized_gateway === 'pay_later') {
932 $row['payment_deadline_days'] = isset($config['payment_deadline_days'])
933 ? max(1, min(60, (int) $config['payment_deadline_days']))
934 : 7;
935 $row['auto_cancel_days'] = isset($config['auto_cancel_days'])
936 ? max(0, min(30, (int) $config['auto_cancel_days']))
937 : 3;
938 $row['require_deposit'] = isset($config['require_deposit']) ? (bool) $config['require_deposit'] : false;
939 $row['deposit_amount'] = isset($config['deposit_amount'])
940 ? max(1, min(50, (int) $config['deposit_amount']))
941 : 10;
942 $row['reminder_days'] = isset($config['reminder_days'])
943 ? sanitize_text_field((string) $config['reminder_days'])
944 : '7,3,1';
945 }
946
947 if ($sanitized_gateway === 'stripe') {
948 $allowedStripeMethods = ['card', 'google_pay', 'apple_pay'];
949 $methodsRaw = isset($config['enabled_methods']) ? (string) $config['enabled_methods'] : '';
950 if ($methodsRaw !== '') {
951 $parts = array_filter(array_map('trim', explode(',', $methodsRaw)));
952 $normalized = [];
953 foreach ($parts as $part) {
954 $slug = strtolower($part);
955 if (in_array($slug, $allowedStripeMethods, true)) {
956 $normalized[] = $slug;
957 }
958 }
959 $row['enabled_methods'] = $normalized !== [] ? implode(',', $normalized) : 'card,google_pay,apple_pay';
960 } else {
961 $row['enabled_methods'] = 'card,google_pay,apple_pay';
962 }
963 foreach (['live_publishable_key', 'live_secret_key', 'test_publishable_key', 'test_secret_key'] as $stripeKey) {
964 if (array_key_exists($stripeKey, $config)) {
965 $row[$stripeKey] = sanitize_text_field((string) $config[$stripeKey]);
966 }
967 }
968 }
969
970 if ($sanitized_gateway === 'razorpay') {
971 $row['key_id'] = isset($config['key_id']) ? sanitize_text_field((string) $config['key_id']) : '';
972 $row['key_secret'] = isset($config['key_secret']) ? sanitize_text_field((string) $config['key_secret']) : '';
973 }
974
975 if ($sanitized_gateway === 'mollie') {
976 $row['api_key'] = isset($config['api_key']) ? sanitize_text_field((string) $config['api_key']) : '';
977 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
978 $allowedMollie = ['creditcard', 'ideal', 'bancontact', 'sofort', 'eps', 'giropay', 'paypal', 'sepadirectdebit'];
979 $row['payment_methods'] = $this->sanitizeGatewayStringList(
980 $config['payment_methods'] ?? [],
981 $allowedMollie,
982 ['creditcard', 'ideal', 'paypal']
983 );
984 }
985
986 if ($sanitized_gateway === 'paystack') {
987 $row['public_key'] = isset($config['public_key']) ? sanitize_text_field((string) $config['public_key']) : '';
988 $row['secret_key'] = isset($config['secret_key']) ? sanitize_text_field((string) $config['secret_key']) : '';
989 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
990 $allowedPaystack = ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'];
991 $row['payment_channels'] = $this->sanitizeGatewayStringList(
992 $config['payment_channels'] ?? [],
993 $allowedPaystack,
994 ['card', 'bank', 'ussd']
995 );
996 unset($row['private_key']);
997 }
998
999 if ($sanitized_gateway === 'square') {
1000 $row['application_id'] = isset($config['application_id']) ? sanitize_text_field((string) $config['application_id']) : '';
1001 $row['access_token'] = isset($config['access_token']) ? sanitize_text_field((string) $config['access_token']) : '';
1002 $row['location_id'] = isset($config['location_id']) ? sanitize_text_field((string) $config['location_id']) : '';
1003 }
1004
1005 if ($sanitized_gateway === 'authorize_net') {
1006 $row['api_login_id'] = isset($config['api_login_id']) ? sanitize_text_field((string) $config['api_login_id']) : '';
1007 $row['transaction_key'] = isset($config['transaction_key']) ? sanitize_text_field((string) $config['transaction_key']) : '';
1008 $row['public_client_key'] = isset($config['public_client_key']) ? sanitize_text_field((string) $config['public_client_key']) : '';
1009 }
1010
1011 if ($sanitized_gateway === 'bank_transfer') {
1012 $row['bank_name'] = isset($config['bank_name']) ? sanitize_text_field((string) $config['bank_name']) : '';
1013 $row['account_name'] = isset($config['account_name']) ? sanitize_text_field((string) $config['account_name']) : '';
1014 $row['account_number'] = isset($config['account_number']) ? sanitize_text_field((string) $config['account_number']) : '';
1015 $row['routing_code'] = isset($config['routing_code']) ? sanitize_text_field((string) $config['routing_code']) : '';
1016 $row['instructions'] = isset($config['instructions']) ? sanitize_textarea_field((string) $config['instructions']) : '';
1017 }
1018
1019 /**
1020 * Allow Pro add-ons or custom code to append keys after core sanitization.
1021 *
1022 * @param array<string, mixed> $row
1023 * @param array<string, mixed> $config
1024 * @return array<string, mixed>
1025 */
1026 $row = apply_filters('yatra_sanitize_gateway_config_row', $row, $sanitized_gateway, $config);
1027
1028 $sanitized[$sanitized_gateway] = $row;
1029 }
1030 return $sanitized;
1031 }
1032
1033 /**
1034 * Normalize multiselect gateway options (Mollie methods, Paystack channels, etc.).
1035 *
1036 * @param mixed $input
1037 * @param array<int, string> $allowed
1038 * @param array<int, string> $default
1039 * @return array<int, string>
1040 */
1041 private function sanitizeGatewayStringList($input, array $allowed, array $default): array
1042 {
1043 if (is_string($input) && $input !== '') {
1044 $input = array_map('trim', explode(',', $input));
1045 }
1046 if (!is_array($input)) {
1047 return $default;
1048 }
1049 $out = [];
1050 foreach ($input as $v) {
1051 $slug = sanitize_key((string) $v);
1052 if ($slug !== '' && in_array($slug, $allowed, true)) {
1053 $out[] = $slug;
1054 }
1055 }
1056 $out = array_values(array_unique($out));
1057
1058 return $out !== [] ? $out : $default;
1059 }
1060
1061 /**
1062 * Sanitize tax rates
1063 */
1064 private function sanitize_tax_rates(array $rates): array
1065 {
1066 $sanitized = [];
1067 foreach ($rates as $country => $rate) {
1068 $sanitized_country = sanitize_text_field($country);
1069 if (is_numeric($rate)) {
1070 $float_rate = (float) $rate;
1071 if ($float_rate >= 0 && $float_rate <= 100) {
1072 $sanitized[$sanitized_country] = $float_rate;
1073 }
1074 }
1075 }
1076 return $sanitized;
1077 }
1078
1079 /**
1080 * Sanitize booking form configuration
1081 */
1082 private function sanitize_booking_form_config(array $config): array
1083 {
1084 $sanitized = [];
1085 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1086 $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1087 $allowed_widths = ['full', 'half', 'third'];
1088
1089 foreach ($config as $form_type => $form_config) {
1090 if (!in_array($form_type, $allowed_form_types, true)) {
1091 continue;
1092 }
1093
1094 $sanitized[$form_type] = [
1095 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1096 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1097 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1098 'fields' => [],
1099 ];
1100
1101 if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
1102 foreach ($form_config['fields'] as $field) {
1103 if (!is_array($field) || empty($field['id'])) {
1104 continue;
1105 }
1106
1107 $sanitized_field = [
1108 'id' => sanitize_key($field['id']),
1109 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1110 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1111 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1112 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1113 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1114 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1115 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? ($field['width'] ?? 'full') : 'full',
1116 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1117 ];
1118
1119 // Handle optional section
1120 if (!empty($field['section'])) {
1121 $sanitized_field['section'] = sanitize_key($field['section']);
1122 }
1123
1124 // Per-traveler targeting — Traveler section only. Whitelist
1125 // the allowed values; only persist the non-default "lead" so
1126 // other sections and existing configs stay byte-identical.
1127 if (
1128 $form_type === 'traveler_form'
1129 && ($field['applies_to'] ?? 'all') === 'lead'
1130 ) {
1131 $sanitized_field['applies_to'] = 'lead';
1132 }
1133
1134 // Handle options for select fields
1135 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1136 $sanitized_field['options'] = [];
1137 foreach ($field['options'] as $option) {
1138 if (is_array($option) && isset($option['value'])) {
1139 $sanitized_field['options'][] = [
1140 'value' => sanitize_key($option['value']),
1141 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1142 ];
1143 }
1144 }
1145 }
1146
1147 // A text block is display-only content placed between fields:
1148 // keep its (safe-HTML) content, and it can never be required.
1149 if ($sanitized_field['type'] === 'text_block') {
1150 $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1151 $sanitized_field['required'] = false;
1152 }
1153
1154 $sanitized[$form_type]['fields'][] = $sanitized_field;
1155 }
1156
1157 // Sort fields by order
1158 usort($sanitized[$form_type]['fields'], function($a, $b) {
1159 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1160 });
1161 }
1162 }
1163
1164 return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1165 }
1166
1167 /**
1168 * Flush rewrite rules
1169 */
1170 public function flush_rewrite_rules(WP_REST_Request $request)
1171 {
1172 try {
1173 // Flush rewrite rules
1174 flush_rewrite_rules(true);
1175
1176 return $this->success_response([
1177 'message' => 'Rewrite rules flushed successfully',
1178 ]);
1179 } catch (\Exception $e) {
1180 return $this->error_response($e->getMessage(), 500);
1181 }
1182 }
1183
1184 /**
1185 * Get list of WordPress pages for booking page selection
1186 * Note: We don't check for shortcode here - it's checked on-demand when user selects a page
1187 */
1188 public function get_pages(WP_REST_Request $request)
1189 {
1190 try {
1191 $pages = get_pages([
1192 'post_status' => 'publish',
1193 'sort_column' => 'post_title',
1194 'sort_order' => 'ASC',
1195 ]);
1196
1197 $page_list = [];
1198 foreach ($pages as $page) {
1199 $page_list[] = [
1200 'id' => $page->ID,
1201 'title' => $page->post_title,
1202 'slug' => $page->post_name,
1203 'url' => get_permalink($page->ID),
1204 ];
1205 }
1206
1207 return $this->success_response($page_list);
1208 } catch (\Exception $e) {
1209 return $this->error_response($e->getMessage(), 500);
1210 }
1211 }
1212
1213 /**
1214 * Check if a page has the booking shortcode
1215 */
1216 public function check_booking_shortcode(WP_REST_Request $request)
1217 {
1218 try {
1219 $page_id = (int) $request->get_param('page_id');
1220
1221 if ($page_id <= 0) {
1222 return $this->error_response('Invalid page ID', 400);
1223 }
1224
1225 $page = get_post($page_id);
1226
1227 if (!$page || $page->post_type !== 'page') {
1228 return $this->error_response('Page not found', 404);
1229 }
1230
1231 $has_shortcode = has_shortcode($page->post_content, 'yatra_booking');
1232
1233 return $this->success_response([
1234 'page_id' => $page_id,
1235 'has_shortcode' => $has_shortcode,
1236 'page_title' => $page->post_title,
1237 'page_url' => get_permalink($page_id),
1238 'edit_url' => get_edit_post_link($page_id, 'raw'),
1239 ]);
1240 } catch (\Exception $e) {
1241 return $this->error_response($e->getMessage(), 500);
1242 }
1243 }
1244
1245 /**
1246 * Insert booking shortcode into a page
1247 */
1248 public function insert_booking_shortcode(WP_REST_Request $request)
1249 {
1250 try {
1251 $page_id = (int) $request->get_param('page_id');
1252
1253 if ($page_id <= 0) {
1254 return $this->error_response('Invalid page ID', 400);
1255 }
1256
1257 $page = get_post($page_id);
1258
1259 if (!$page || $page->post_type !== 'page') {
1260 return $this->error_response('Page not found', 404);
1261 }
1262
1263 // Check if shortcode already exists
1264 if (has_shortcode($page->post_content, 'yatra_booking')) {
1265 return $this->success_response([
1266 'message' => 'Shortcode already exists on this page',
1267 'page_id' => $page_id,
1268 'already_exists' => true,
1269 ]);
1270 }
1271
1272 // Append shortcode to page content
1273 $new_content = $page->post_content . "\n\n[yatra_booking]";
1274
1275 $result = wp_update_post([
1276 'ID' => $page_id,
1277 'post_content' => $new_content,
1278 ], true);
1279
1280 if (is_wp_error($result)) {
1281 return $this->error_response($result->get_error_message(), 500);
1282 }
1283
1284 return $this->success_response([
1285 'message' => 'Shortcode added successfully',
1286 'page_id' => $page_id,
1287 'page_url' => get_permalink($page_id),
1288 ]);
1289 } catch (\Exception $e) {
1290 return $this->error_response($e->getMessage(), 500);
1291 }
1292 }
1293
1294 /**
1295 * Keep Settings → Customer "account page" path aligned with {@see RouteMatcher} / {@see Router} (yatra_account_base).
1296 *
1297 * @param array<string, mixed> $settings
1298 * @return array<string, mixed>
1299 */
1300 private function syncAccountRouteSettingsForResponse(array $settings): array
1301 {
1302 // Prefer the full saved path so admin "View" matches Settings → Customer (not only yatra_account_base slug).
1303 $savedPath = get_option('yatra_customer_account_page', '');
1304 if (is_string($savedPath) && $savedPath !== '' && $savedPath !== '0') {
1305 $normalized = '/' . trim(str_replace('\\', '/', $savedPath), '/');
1306 if ($normalized === '/') {
1307 $normalized = '/my-account';
1308 }
1309 $settings['customer_account_page'] = $normalized;
1310
1311 return $settings;
1312 }
1313
1314 $stored = get_option('yatra_account_base', '');
1315 if (is_string($stored) && $stored !== '') {
1316 $settings['customer_account_page'] = '/' . $stored;
1317
1318 return $settings;
1319 }
1320
1321 $cpp = (string) ($settings['customer_account_page'] ?? '');
1322 $slug = self::accountSlugFromCustomerAccountPath($cpp !== '' ? $cpp : '/account');
1323 update_option('yatra_account_base', $slug);
1324 $settings['customer_account_page'] = '/' . $slug;
1325
1326 return $settings;
1327 }
1328
1329 private function persistAccountBaseFromCustomerAccountPage(): void
1330 {
1331 $cpp = (string) get_option('yatra_customer_account_page', '');
1332 update_option('yatra_account_base', self::accountSlugFromCustomerAccountPath($cpp));
1333 }
1334
1335 private static function accountSlugFromCustomerAccountPath(string $path): string
1336 {
1337 $path = trim(str_replace('\\', '/', $path), '/');
1338 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
1339 $segment = $parts !== [] ? end($parts) : 'account';
1340 $slug = sanitize_title($segment);
1341
1342 return $slug !== '' ? $slug : 'account';
1343 }
1344 }
1345
1346