PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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.11, at app/Controllers/SettingsController.php

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