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

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