PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Migrations / SettingsMigration.php

SettingsMigration.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Migrations/SettingsMigration.php

1,699 lines 76.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Migration;
4
5 use Yatra\Migration\MigrationProgress;
6 use Yatra\Utils\Logger;
7
8 class SettingsMigration extends BaseMigration
9 {
10 /**
11 * True when legacy permalink slug options were written; triggers a rewrite flush after run().
12 */
13 private bool $rewriteRulesNeedFlush = false;
14
15 public function __construct(MigrationProgress $service)
16 {
17 parent::__construct($service);
18 }
19
20 public function run(): array
21 {
22 global $wpdb;
23
24 $migrated = 0;
25 $skipped = 0;
26 $failed = 0;
27
28 try {
29 // Map old settings to new settings
30 $settingsMap = $this->getSettingsMap();
31
32 $total = count($settingsMap);
33
34 Logger::info("Starting settings migration", [
35 'source' => 'migration',
36 'total_simple_settings' => $total,
37 'force_migration' => $this->isForceMigration()
38 ]);
39
40 // Debug: Check what old Yatra settings actually exist
41 global $wpdb;
42 $existingOldSettings = $wpdb->get_col(
43 "SELECT option_name FROM {$wpdb->options}
44 WHERE option_name LIKE 'yatra_%'
45 AND option_name NOT LIKE '%gateway_configs%'
46 AND option_name NOT LIKE '%migration_%'
47 ORDER BY option_name"
48 );
49
50 Logger::info("Found " . count($existingOldSettings) . " existing old Yatra settings", [
51 'source' => 'migration',
52 'existing_settings' => $existingOldSettings
53 ]);
54
55 // Debug: Log specific gateway-related settings
56 $gatewaySettings = ['yatra_payment_gateways', 'yatra_paypal_settings', 'yatra_stripe_settings', 'yatra_authorize_net_settings'];
57 foreach ($gatewaySettings as $key) {
58 $value = get_option($key, 'NOT_FOUND');
59 $valueType = is_array($value) ? '[ARRAY:' . count($value) . ']' : (is_serialized($value) ? '[SERIALIZED]' : $value);
60 Logger::debug("Gateway setting - {$key}: {$valueType}", ['source' => 'migration']);
61 }
62
63 foreach ($settingsMap as $mapping) {
64 try {
65 $oldKey = $mapping['old'];
66 $newKey = $mapping['new'];
67 $transform = $mapping['transform'] ?? null;
68 $default = $mapping['default'] ?? null;
69
70 // Get old setting value
71 $oldValue = get_option($oldKey, null);
72
73 if ($oldValue === null && $default !== null) {
74 $oldValue = $default;
75 }
76
77 if ($oldValue === null) {
78 $skipped++;
79 Logger::debug("Setting skipped (not found): {$oldKey}", ['source' => 'migration']);
80 continue;
81 }
82
83 // Transform value if needed
84 $newValue = $transform ? $transform($oldValue) : $oldValue;
85
86 // Check if new setting already exists.
87 // NOTE: Since many option keys are shared between old and new plugin (same
88 // option_name for the same feature), the new plugin's install routine may
89 // have already written a default value. We always overwrite to ensure the
90 // user's real data from the old plugin wins — unless it's identical.
91 $existingValue = get_option($newKey, null);
92
93 if ($existingValue !== null && !$this->isForceMigration()) {
94 // If the values are already identical, just count as migrated to avoid
95 // misleading 'skipped 30 settings' message in the UI.
96 if (serialize($existingValue) === serialize($newValue)) {
97 $migrated++;
98 Logger::debug("Setting already matches, counted as migrated: {$oldKey} -> {$newKey}", ['source' => 'migration']);
99 $this->updateProgress('settings', 'running', $migrated, $skipped, $failed, $total, null, null);
100 continue;
101 }
102 // Values differ — for same-key settings the old data should win.
103 // Only skip if old and new keys are truly different option names
104 // (meaning the new plugin intentionally renamed them).
105 if ($oldKey !== $newKey) {
106 $skipped++;
107 Logger::debug("Setting skipped (already exists, different key): {$oldKey} -> {$newKey}", ['source' => 'migration']);
108 $this->updateProgress('settings', 'running', $migrated, $skipped, $failed, $total, null, null);
109 continue;
110 }
111 // Same key in both systems — overwrite with old user data.
112 }
113
114 // Update or add new setting
115 update_option($newKey, $newValue);
116
117 if ($existingValue !== null && $this->isForceMigration()) {
118 Logger::info("Setting force-migrated (overwritten): {$oldKey} -> {$newKey}", ['source' => 'migration']);
119 } else {
120 Logger::info("Setting migrated: {$oldKey} -> {$newKey}", ['source' => 'migration']);
121 }
122
123 $migrated++;
124
125 } catch (\Exception $e) {
126 $failed++;
127 Logger::error("Setting migration exception", [
128 'source' => 'migration',
129 'old_key' => $oldKey ?? 'unknown',
130 'error' => $e->getMessage()
131 ]);
132 }
133
134 $this->updateProgress('settings', 'running', $migrated, $skipped, $failed, $total, null, null);
135 }
136
137 // Migrate complex settings (arrays/objects) and count them
138 Logger::info("Starting complex settings migration", ['source' => 'migration']);
139 $complexResults = $this->migrateComplexSettings();
140 $migrated += $complexResults['migrated'];
141 $skipped += $complexResults['skipped'];
142 $failed += $complexResults['failed'];
143
144 Logger::info("Settings migration completed", [
145 'source' => 'migration',
146 'migrated' => $migrated,
147 'skipped' => $skipped,
148 'failed' => $failed
149 ]);
150
151 if ($this->rewriteRulesNeedFlush && function_exists('flush_rewrite_rules')) {
152 flush_rewrite_rules(true);
153 Logger::info('Flushed rewrite rules after legacy permalink slug migration', [
154 'source' => 'migration',
155 ]);
156 $this->rewriteRulesNeedFlush = false;
157 }
158
159 if (class_exists(\Yatra\Services\SettingsService::class)) {
160 \Yatra\Services\SettingsService::reload();
161 }
162
163 } catch (\Exception $e) {
164 Logger::error("Settings migration failed", [
165 'source' => 'migration',
166 'error' => $e->getMessage()
167 ]);
168 }
169
170 return [
171 'migrated' => $migrated,
172 'skipped' => $skipped,
173 'failed' => $failed,
174 ];
175 }
176
177 /**
178 * Get settings mapping from old to new
179 *
180 * These are the ACTUAL old Yatra option keys verified from old plugin source code.
181 * Old keys found in: class-yatra-install.php, class-yatra-setup-wizard.php,
182 * yatra-template-functions.php, class-yatra-email.php, yatra-pricing-functions.php,
183 * hooks/yatra-design-hooks.php, class-yatra-assets.php, functions.php
184 */
185 private function getSettingsMap(): array
186 {
187 return [
188 // Currency Settings (verified from setup wizard + template functions)
189 [
190 'old' => 'yatra_currency',
191 'new' => 'yatra_currency',
192 ],
193 [
194 'old' => 'yatra_currency_position',
195 'new' => 'yatra_currency_position',
196 ],
197 [
198 'old' => 'yatra_thousand_separator',
199 'new' => 'yatra_thousand_separator',
200 ],
201 [
202 'old' => 'yatra_decimal_separator',
203 'new' => 'yatra_decimal_separator',
204 ],
205 // yatra_price_number_decimals → yatra_decimal_places: see migrateLegacyRenamedFreeOptions()
206 [
207 'old' => 'yatra_currency_symbol_type',
208 'new' => 'yatra_currency_symbol_type',
209 ],
210
211 // Page Settings (verified from class-yatra-install.php)
212 [
213 'old' => 'yatra_checkout_page',
214 'new' => 'yatra_checkout_page',
215 'transform' => function($value) {
216 return intval($value);
217 }
218 ],
219 [
220 'old' => 'yatra_cart_page',
221 'new' => 'yatra_cart_page',
222 'transform' => function($value) {
223 return intval($value);
224 }
225 ],
226 [
227 'old' => 'yatra_thankyou_page',
228 'new' => 'yatra_thankyou_page',
229 'transform' => function($value) {
230 return intval($value);
231 }
232 ],
233 [
234 'old' => 'yatra_my_account_page',
235 'new' => 'yatra_my_account_page',
236 'transform' => function($value) {
237 return intval($value);
238 }
239 ],
240 [
241 'old' => 'yatra_failed_transaction_page',
242 'new' => 'yatra_failed_transaction_page',
243 'transform' => function($value) {
244 return intval($value);
245 }
246 ],
247
248 // Booking/Checkout Settings (verified from class-yatra-install.php + hooks)
249 // yatra_enable_guest_checkout → allow_guest_checkout + enable_guest_booking: migrateLegacyRenamedFreeOptions()
250 [
251 'old' => 'yatra_booknow_button_text',
252 'new' => 'yatra_booknow_button_text',
253 ],
254 [
255 'old' => 'yatra_booknow_loading_text',
256 'new' => 'yatra_booknow_loading_text',
257 ],
258 [
259 'old' => 'yatra_booking_form_title_text',
260 'new' => 'yatra_booking_form_title_text',
261 ],
262 [
263 'old' => 'yatra_enquiry_form_title_text',
264 'new' => 'yatra_enquiry_form_title_text',
265 ],
266 [
267 'old' => 'yatra_enquiry_button_text',
268 'new' => 'yatra_enquiry_button_text',
269 ],
270 [
271 'old' => 'yatra_select_date_title',
272 'new' => 'yatra_select_date_title',
273 ],
274 [
275 'old' => 'yatra_custom_attributes_title_text',
276 'new' => 'yatra_custom_attributes_title_text',
277 ],
278 [
279 'old' => 'yatra_update_cart_text',
280 'new' => 'yatra_update_cart_text',
281 ],
282 [
283 'old' => 'yatra_proceed_to_checkout_text',
284 'new' => 'yatra_proceed_to_checkout_text',
285 ],
286 [
287 'old' => 'yatra_order_booking_text',
288 'new' => 'yatra_order_booking_text',
289 ],
290
291 // Payment Gateway Settings (verified from function-yatra-payments.php)
292 // yatra_payment_gateway_test_mode → yatra_payment_test_mode: migrateLegacyRenamedFreeOptions()
293 // yatra_payment_gateway_enable_logging → yatra_enable_logging: migrateLegacyRenamedFreeOptions()
294
295 // Email Settings (verified from class-yatra-email.php)
296 [
297 'old' => 'yatra_email_from_name',
298 'new' => 'yatra_email_from_name',
299 ],
300 [
301 'old' => 'yatra_email_from_address',
302 'new' => 'yatra_email_from_address',
303 ],
304 [
305 'old' => 'yatra_admin_email_recipient_lists',
306 'new' => 'yatra_admin_email_recipient_lists',
307 ],
308 [
309 'old' => 'yatra_disable_all_email',
310 'new' => 'yatra_disable_all_email',
311 'transform' => function($value) {
312 return $value === 'yes' || $value === true || $value === 1;
313 }
314 ],
315
316 // yatra_payment_tax_rate → yatra_tax_rate: migrateLegacyRenamedFreeOptions()
317
318 // Layout/Design Settings (verified from hooks and setup wizard)
319 [
320 'old' => 'yatra_page_container_class',
321 'new' => 'yatra_page_container_class',
322 ],
323 [
324 'old' => 'yatra_setting_layouts_single_tour_tab_layout',
325 'new' => 'yatra_setting_layouts_single_tour_tab_layout',
326 ],
327 [
328 'old' => 'yatra_setting_layouts_tour_archive',
329 'new' => 'yatra_setting_layouts_tour_archive',
330 ],
331 [
332 'old' => 'yatra_design_primary_color',
333 'new' => 'yatra_design_primary_color',
334 ],
335 [
336 'old' => 'yatra_available_for_booking_color',
337 'new' => 'yatra_available_for_booking_color',
338 ],
339 [
340 'old' => 'yatra_available_for_enquiry_only_color',
341 'new' => 'yatra_available_for_enquiry_only_color',
342 ],
343 [
344 'old' => 'yatra_not_available_for_booking_enquiry_color',
345 'new' => 'yatra_not_available_for_booking_enquiry_color',
346 ],
347
348 // Misc Settings (verified from various files)
349 [
350 'old' => 'yatra_date_selection_type',
351 'new' => 'yatra_date_selection_type',
352 ],
353 [
354 'old' => 'yatra_enquiry_form_show',
355 'new' => 'yatra_enquiry_form_show',
356 'transform' => function($value) {
357 return $value === 'yes' || $value === true || $value === 1;
358 }
359 ],
360 [
361 'old' => 'yatra_log_options',
362 'new' => 'yatra_log_options',
363 ],
364
365 // ── Additional settings verified from yatra-misc-functions.php and form handler ──
366
367 // Tour listing "View Details" button text
368 [
369 'old' => 'yatra_tour_view_details_button_text',
370 'new' => 'yatra_tour_view_details_button_text',
371 ],
372
373 // Checkout legal agreement toggles (class-yatra-form-handler.php)
374 [
375 'old' => 'yatra_checkout_show_agree_to_privacy_policy',
376 'new' => 'yatra_checkout_show_agree_to_privacy_policy',
377 'transform' => function($value) {
378 return $value === 'yes' || $value === true || $value === 1;
379 }
380 ],
381 [
382 'old' => 'yatra_checkout_show_agree_to_terms_policy',
383 'new' => 'yatra_checkout_show_agree_to_terms_policy',
384 'transform' => function($value) {
385 return $value === 'yes' || $value === true || $value === 1;
386 }
387 ],
388
389 // Date/time display formats (used in template-tags.php)
390 [
391 'old' => 'yatra_date_format',
392 'new' => 'yatra_date_format',
393 ],
394 [
395 'old' => 'yatra_time_format',
396 'new' => 'yatra_time_format',
397 ],
398 ];
399 }
400
401 /**
402 * Copy legacy public URL slug settings into 3.x wp_options (yatra_*_base) and flush rewrites.
403 *
404 * Sources (in order):
405 * 1) {@see get_option('yatra_permalinks')} — 1.x/2.x array from Settings → Permalinks (and forks that
406 * stored extra keys). Values may be serialized strings; non-scalar entries are ignored.
407 * 2) Standalone options — rare cases where yatra_tour_base (or others) existed outside the array.
408 *
409 * 1.x core only persisted: yatra_tour_base, yatra_destination_base, yatra_activity_base,
410 * yatra_attributes_base (see yatra-old class-yatra-admin-permalinks.php). 3.x uses yatra_trip_base
411 * for trips; tour_* / trip_* aliases are all mapped here.
412 *
413 * @return array{migrated: int, skipped: int}
414 */
415 private function migrateLegacyPermalinkBases(): array
416 {
417 $writes = 0;
418 $sanitize = static function ($value): string {
419 if ($value === null || $value === false || $value === '') {
420 return '';
421 }
422 if (!is_scalar($value)) {
423 return '';
424 }
425 $s = trim((string) $value);
426 $s = function_exists('untrailingslashit') ? untrailingslashit($s) : rtrim($s, '/');
427 $s = trim($s, '/');
428 $s = preg_replace('/[^a-z0-9_-]/i', '', $s) ?: '';
429
430 return $s;
431 };
432
433 $setSlugOption = function (string $optionName, $raw) use (&$writes, $sanitize): void {
434 $slug = $sanitize($raw);
435 if ($slug === '') {
436 return;
437 }
438 $current = get_option($optionName, '');
439 $curNorm = is_string($current) ? $sanitize($current) : '';
440 if ($curNorm === $slug) {
441 return;
442 }
443 update_option($optionName, $slug);
444 $writes++;
445 $this->rewriteRulesNeedFlush = true;
446 };
447
448 $rawPermalinks = get_option('yatra_permalinks', null);
449 $oldPermalinks = [];
450 if (is_string($rawPermalinks)) {
451 $maybe = maybe_unserialize($rawPermalinks);
452 $oldPermalinks = is_array($maybe) ? $maybe : [];
453 } elseif (is_array($rawPermalinks)) {
454 $oldPermalinks = $rawPermalinks;
455 }
456
457 // One winning value per 3.x option: prefer canonical yatra_* keys, then short aliases (1.x/2.x/forks).
458 $pickScalar = static function (array $row, array $keys) {
459 foreach ($keys as $key) {
460 if (!empty($row[$key]) && is_scalar($row[$key])) {
461 return $row[$key];
462 }
463 }
464
465 return null;
466 };
467
468 $groups = [
469 'yatra_trip_base' => [
470 'yatra_tour_base',
471 'yatra_trip_base',
472 'yatra_tours_base',
473 'tour_base',
474 'trip_base',
475 ],
476 'yatra_destination_base' => ['yatra_destination_base', 'destination_base'],
477 'yatra_activity_base' => ['yatra_activity_base', 'activity_base'],
478 'yatra_attributes_base' => [
479 'yatra_attributes_base',
480 'yatra_attribute_base',
481 'attributes_base',
482 'attribute_base',
483 ],
484 'yatra_booking_base' => ['yatra_booking_base', 'booking_base'],
485 'yatra_trip_category_base' => [
486 'yatra_trip_category_base',
487 'yatra_tour_category_base',
488 'trip_category_base',
489 'tour_category_base',
490 'yatra_category_base',
491 'category_base',
492 ],
493 'yatra_difficulty_base' => ['yatra_difficulty_base', 'difficulty_base'],
494 'yatra_account_base' => ['yatra_account_base', 'yatra_my_account_base', 'account_base'],
495 ];
496
497 foreach ($groups as $optionName => $legacyKeys) {
498 $raw = $pickScalar($oldPermalinks, $legacyKeys);
499 if ($raw !== null) {
500 $setSlugOption($optionName, $raw);
501 }
502 }
503
504 // Orphan top-level options (imports, partial upgrades, or custom code) — do not override array data.
505 $tourFromArray = $pickScalar($oldPermalinks, $groups['yatra_trip_base']) !== null;
506 if (!$tourFromArray) {
507 $orphanTour = get_option('yatra_tour_base', '');
508 if (is_string($orphanTour) && $orphanTour !== '') {
509 $setSlugOption('yatra_trip_base', $orphanTour);
510 }
511 }
512
513 $standaloneBases = [
514 ['yatra_destination_base', 'yatra_destination_base', ['yatra_destination_base', 'destination_base']],
515 ['yatra_activity_base', 'yatra_activity_base', ['yatra_activity_base', 'activity_base']],
516 ['yatra_attributes_base', 'yatra_attributes_base', ['yatra_attributes_base', 'yatra_attribute_base', 'attributes_base', 'attribute_base']],
517 ['yatra_booking_base', 'yatra_booking_base', ['yatra_booking_base', 'booking_base']],
518 ['yatra_trip_category_base', 'yatra_trip_category_base', [
519 'yatra_trip_category_base', 'yatra_tour_category_base', 'trip_category_base', 'tour_category_base',
520 'yatra_category_base', 'category_base',
521 ]],
522 ['yatra_difficulty_base', 'yatra_difficulty_base', ['yatra_difficulty_base', 'difficulty_base']],
523 ['yatra_account_base', 'yatra_account_base', ['yatra_account_base', 'account_base', 'yatra_my_account_base']],
524 ];
525 foreach ($standaloneBases as [$optionKey, $destOption, $arrayKeys]) {
526 $fromArray = false;
527 foreach ($arrayKeys as $ak) {
528 if (!empty($oldPermalinks[$ak])) {
529 $fromArray = true;
530 break;
531 }
532 }
533 if ($fromArray) {
534 continue;
535 }
536 $val = get_option($optionKey, '');
537 if (!is_string($val) || $val === '') {
538 continue;
539 }
540 $setSlugOption($destOption, $val);
541 }
542
543 $hadPermalinkOption = $rawPermalinks !== null && $rawPermalinks !== false && $rawPermalinks !== '';
544 $skipped = ($writes === 0 && !$hadPermalinkOption) ? 1 : 0;
545
546 if ($writes > 0) {
547 Logger::info("Migrated {$writes} permalink slug option(s) from legacy Yatra permalink data", [
548 'source' => 'migration',
549 ]);
550 } elseif ($hadPermalinkOption && $writes === 0) {
551 Logger::debug('Legacy yatra_permalinks present but contained no migratable slug values', ['source' => 'migration']);
552 } elseif ($skipped) {
553 Logger::debug('Skipped permalinks (no yatra_permalinks option and no standalone bases)', ['source' => 'migration']);
554 }
555
556 return [
557 'migrated' => $writes,
558 'skipped' => $skipped,
559 ];
560 }
561
562 /**
563 * Free 2.x → 3.x option renames that SettingsService actually reads.
564 *
565 * The simple settings loop skips when old_key !== new_key and the 3.x option already exists with a
566 * different value — which would leave these stuck on defaults forever after upgrade.
567 */
568 private function migrateLegacyRenamedFreeOptions(): int
569 {
570 $n = 0;
571
572 $legacyGuest = get_option('yatra_enable_guest_checkout', null);
573 if ($legacyGuest !== null && $legacyGuest !== false && $legacyGuest !== '') {
574 $on = $legacyGuest === 'yes' || $legacyGuest === true || $legacyGuest === 1 || $legacyGuest === '1';
575 update_option('yatra_allow_guest_checkout', $on);
576 update_option('yatra_enable_guest_booking', $on);
577 Logger::info('Migrated yatra_enable_guest_checkout → yatra_allow_guest_checkout + yatra_enable_guest_booking', [
578 'source' => 'migration',
579 'enabled' => $on,
580 ]);
581 $n++;
582 }
583
584 $legacyDecimals = get_option('yatra_price_number_decimals', null);
585 if ($legacyDecimals !== null && $legacyDecimals !== false && $legacyDecimals !== '') {
586 update_option('yatra_decimal_places', max(0, min(10, (int) $legacyDecimals)));
587 Logger::info('Migrated yatra_price_number_decimals → yatra_decimal_places', [
588 'source' => 'migration',
589 ]);
590 $n++;
591 }
592
593 $legacyTax = get_option('yatra_payment_tax_rate', null);
594 if ($legacyTax !== null && $legacyTax !== false && $legacyTax !== '') {
595 update_option('yatra_tax_rate', (float) $legacyTax);
596 // Old system treated tax as enabled when rate > 0. New system also requires enable_tax flag.
597 if ((float) $legacyTax > 0) {
598 $existingEnable = get_option('yatra_enable_tax', null);
599 if ($existingEnable === null || $existingEnable === '' || $this->isForceMigration()) {
600 update_option('yatra_enable_tax', true);
601 }
602 }
603 Logger::info('Migrated yatra_payment_tax_rate → yatra_tax_rate', [
604 'source' => 'migration',
605 ]);
606 $n++;
607 }
608
609 // 2.x stored payment test mode under yatra_payment_gateway_test_mode ('yes'/'no'); 3.x SettingsService reads yatra_payment_test_mode (bool).
610 $legacyPayTest = get_option('yatra_payment_gateway_test_mode', null);
611 if ($legacyPayTest !== null && $legacyPayTest !== false && $legacyPayTest !== '') {
612 $on = $legacyPayTest === 'yes' || $legacyPayTest === true || $legacyPayTest === 1 || $legacyPayTest === '1';
613 update_option('yatra_payment_test_mode', $on);
614 Logger::info('Migrated yatra_payment_gateway_test_mode → yatra_payment_test_mode', [
615 'source' => 'migration',
616 'test_mode' => $on,
617 ]);
618 $n++;
619 }
620
621 // 2.x gateway log toggle → 3.x yatra_enable_logging (LoggingService / Settings UI).
622 $legacyGwLog = get_option('yatra_payment_gateway_enable_logging', null);
623 if ($legacyGwLog !== null && $legacyGwLog !== false && $legacyGwLog !== '') {
624 $on = $legacyGwLog === 'yes' || $legacyGwLog === true || $legacyGwLog === 1 || $legacyGwLog === '1';
625 update_option('yatra_enable_logging', $on);
626 Logger::info('Migrated yatra_payment_gateway_enable_logging → yatra_enable_logging', [
627 'source' => 'migration',
628 'enabled' => $on,
629 ]);
630 $n++;
631 }
632
633 return $n;
634 }
635
636 /**
637 * Map Yatra 2.x email notification checkboxes to 3.x template enable flags.
638 *
639 * 2.x keys (class-yatra-settings-emails.php) do not match 3.x; without this, fresh 3.x defaults (all on)
640 * would ignore a site that had turned emails off.
641 *
642 * 3.x naming note: {@see TransactionalEmailTemplateService::typeToSettingsKeys()} — email_template_confirmation
643 * drives payment confirmation, not “booking status change”; we only map toggles where semantics align.
644 */
645 private function migrateLegacyEmailTemplateToggles(): int
646 {
647 $n = 0;
648 $yes = static function ($v): bool {
649 return $v === 'yes' || $v === true || $v === 1 || $v === '1';
650 };
651
652 $bc = get_option('yatra_enable_booking_notification_email_for_customer', null);
653 $sc = get_option('yatra_enable_booking_status_change_notification_email_for_customer', null);
654 if ($bc !== null || $sc !== null) {
655 $customerOn = ($bc !== null && $yes($bc)) || ($sc !== null && $yes($sc));
656 update_option('yatra_email_template_booking', $customerOn);
657 Logger::info('Migrated legacy customer booking/status email toggles → yatra_email_template_booking', [
658 'source' => 'migration',
659 'enabled' => $customerOn,
660 ]);
661 $n++;
662 }
663
664 $ba = get_option('yatra_enable_booking_notification_email_for_admin', null);
665 if ($ba !== null) {
666 update_option('yatra_email_template_admin_new_booking', $yes($ba));
667 Logger::info('Migrated legacy admin booking notification toggle → yatra_email_template_admin_new_booking', [
668 'source' => 'migration',
669 'enabled' => $yes($ba),
670 ]);
671 $n++;
672 }
673
674 // Admin “booking status change” emails have no separate flag in 3.x core; do not map to
675 // email_template_admin_cancellation (that is cancellation-specific).
676
677 return $n;
678 }
679
680 /**
681 * Migrate complex settings (arrays, objects, etc.)
682 *
683 * Verified from old plugin source:
684 * - yatra_payment_gateways: array of active gateway IDs (from class-yatra-install.php)
685 * - yatra_permalinks: array with tour/trip, booking, category, difficulty, account bases, etc.
686 * (from admin/class-yatra-admin-permalinks.php; keys may be prefixed or short forms)
687 * - Individual gateway settings (yatra_paypal_settings, yatra_stripe_settings, etc.)
688 *
689 * @return array ['migrated' => int, 'skipped' => int, 'failed' => int]
690 */
691 private function migrateComplexSettings(): array
692 {
693 $migrated = 0;
694 $skipped = 0;
695 $failed = 0;
696
697 // Migrate active payment gateways list.
698 //
699 // OLD format (class-yatra-install.php line 197):
700 // yatra_payment_gateways = ['booking_only' => 'yes', 'paypal' => 'yes']
701 // Active gateways are retrieved via array_keys() — the value is always 'yes'.
702 //
703 // NEW format expected by yatra 3.x:
704 // yatra_payment_gateways = ['pay_later', 'paypal'] (indexed array)
705 //
706 // We must convert the associative slug=>yes map to a plain indexed list.
707 $oldGateways = get_option('yatra_payment_gateways', []);
708 if (!empty($oldGateways) && is_array($oldGateways)) {
709 // Filter out any disabled gateways (value != 'yes') and extract just the slugs.
710 $activeGatewayIds = array_keys(array_filter($oldGateways, function($v) {
711 return $v === 'yes' || $v === true || $v === 1;
712 }));
713 // Normalize legacy slug booking_only → pay_later
714 $activeGatewayIds = array_map(
715 static fn ($slug) => $slug === 'booking_only' ? 'pay_later' : $slug,
716 $activeGatewayIds
717 );
718 update_option('yatra_payment_gateways', array_values(array_unique($activeGatewayIds)));
719 $migrated++;
720 Logger::info("Migrated active payment gateways (indexed)", [
721 'source' => 'migration',
722 'gateways' => $activeGatewayIds
723 ]);
724 } else {
725 $skipped++;
726 Logger::debug("Skipped payment gateways (not found or empty)", ['source' => 'migration']);
727 }
728
729 // Migrate payment gateway configurations
730 $gatewayResults = $this->migratePaymentGatewayConfigs();
731 $migrated += $gatewayResults['migrated'];
732 $skipped += $gatewayResults['skipped'];
733
734 $permalinkResults = $this->migrateLegacyPermalinkBases();
735 $migrated += $permalinkResults['migrated'];
736 $skipped += $permalinkResults['skipped'];
737
738 // 2.x options renamed in 3.x (must not rely on simple map — getSettingsMap skips when new key already differs).
739 $migrated += $this->migrateLegacyRenamedFreeOptions();
740
741 // Migrate enquiry/booking form settings
742 $enquiryResults = $this->migrateEnquirySettings();
743 $migrated += $enquiryResults['migrated'];
744 $skipped += $enquiryResults['skipped'];
745
746 // 2.x per-template email on/off → 3.x yatra_email_template_* flags (TransactionalEmailTemplateService).
747 $migrated += $this->migrateLegacyEmailTemplateToggles();
748
749 // Legacy Pro Google Calendar OAuth options → new Pro 3.x option names (no separate migration step).
750 $this->migrateLegacyProGoogleCalendarTokens();
751
752 // Legacy Pro license options (yatra_pro_license_key/status/...) → new Pro 3.x license storage.
753 $this->migrateLegacyProLicenseOptions();
754
755 // Legacy Pro review settings + partial payment (wp_options) → Yatra 3.x core settings keys.
756 $this->migrateLegacyReviewAndPartialPaymentOptions();
757
758 return [
759 'migrated' => $migrated,
760 'skipped' => $skipped,
761 'failed' => $failed,
762 ];
763 }
764
765 /**
766 * Map legacy Pro Google Calendar token options to the keys used by Yatra Pro 3.x SettingsRepository.
767 * Skips when Pro 3.0+ is not active (same rule as other Pro table migrations).
768 */
769 private function migrateLegacyProGoogleCalendarTokens(): void
770 {
771 $refresh = (string) get_option('yatra_google_calendar_refresh_token', '');
772 $access = (string) get_option('yatra_google_calendar_access_token', '');
773 $expiresIn = get_option('yatra_google_calendar_expires_in', '');
774 $legacyEnable = get_option('yatra_enable_google_calendar', null);
775
776 if ($refresh === '' && $access === '' && ($expiresIn === '' || $expiresIn === null) && $legacyEnable === null) {
777 return;
778 }
779
780 $pro = ProMigrationReadiness::getState();
781 if (!$pro['ready']) {
782 Logger::warning('Skipping legacy Google Calendar token migration: Yatra Pro 3.0+ not ready', [
783 'source' => 'migration',
784 'pro_migration' => $pro,
785 ]);
786
787 return;
788 }
789
790 $tokenExpiresAt = get_option('yatra_google_calendar_token_expires_at', null);
791 if (($tokenExpiresAt === null || $tokenExpiresAt === '' || $this->isForceMigration()) && $expiresIn !== '' && $expiresIn !== null) {
792 $n = (int) $expiresIn;
793 if ($n > 0) {
794 update_option('yatra_google_calendar_token_expires_at', time() + $n);
795 }
796 }
797
798 if ($legacyEnable !== null) {
799 $enabled = ((string) $legacyEnable === '1' || $legacyEnable === 1 || $legacyEnable === true) ? 'yes' : 'no';
800 $existing = get_option('yatra_google_calendar_enabled', null);
801 if ($existing === null || $existing === '' || $this->isForceMigration()) {
802 update_option('yatra_google_calendar_enabled', $enabled);
803 }
804 }
805
806 Logger::info('Migrated legacy Google Calendar options for Yatra Pro 3.x.', ['source' => 'migration']);
807 }
808
809 /**
810 * Migrate legacy Yatra Pro license options.
811 *
812 * Old Pro stored UI values in:
813 * - yatra_pro_license_key
814 * - yatra_pro_license_status
815 * - yatra_pro_license_expires (optional; may be empty)
816 *
817 * New Pro's updater reads from unified:
818 * - yatra_license['yatra-pro'] = ['license_key','status','server_response','last_checked']
819 *
820 * We sync the key/status into yatra_license so updates/licensing work, and also keep the
821 * yatra_pro_* options populated because Pro's SettingsController currently returns them.
822 *
823 * IMPORTANT: We do NOT auto-activate (network request) during migration. Activation is done via Pro UI/API.
824 */
825 private function migrateLegacyProLicenseOptions(): void
826 {
827 $legacyKey = (string) get_option('yatra_pro_license_key', '');
828 $legacyStatus = (string) get_option('yatra_pro_license_status', '');
829 $legacyExpires = (string) get_option('yatra_pro_license_expires', '');
830
831 if ($legacyKey === '' && $legacyStatus === '' && $legacyExpires === '') {
832 return;
833 }
834
835 // If Pro isn't ready, skip writing to unified store (keeps behavior consistent with other Pro migrations).
836 $pro = ProMigrationReadiness::getState();
837 if (!$pro['ready']) {
838 Logger::warning('Skipping legacy Pro license migration: Yatra Pro 3.0+ not ready', [
839 'source' => 'migration',
840 'pro_migration' => $pro,
841 ]);
842 return;
843 }
844
845 $pluginSlug = 'yatra-pro';
846 $license = get_option('yatra_license', []);
847 $license = is_array($license) ? $license : [];
848 $existing = isset($license[$pluginSlug]) && is_array($license[$pluginSlug]) ? $license[$pluginSlug] : [];
849
850 $normalizedStatus = strtolower(trim($legacyStatus));
851 $map = [
852 'valid' => 'active',
853 'active' => 'active',
854 'inactive' => 'inactive',
855 'expired' => 'expired',
856 'disabled' => 'disabled',
857 'invalid' => 'invalid',
858 ];
859 if ($normalizedStatus === '') {
860 $normalizedStatus = 'inactive';
861 }
862 $normalizedStatus = $map[$normalizedStatus] ?? $normalizedStatus;
863
864 $shouldWrite = $this->isForceMigration()
865 || empty($existing['license_key'])
866 || empty($existing['status']);
867
868 if ($shouldWrite) {
869 $license[$pluginSlug] = array_merge($existing, [
870 'license_key' => $legacyKey !== '' ? $legacyKey : (string) ($existing['license_key'] ?? ''),
871 'status' => $normalizedStatus !== '' ? $normalizedStatus : (string) ($existing['status'] ?? 'inactive'),
872 'server_response' => is_array($existing['server_response'] ?? null) ? $existing['server_response'] : [],
873 'last_checked' => (int) ($existing['last_checked'] ?? 0) ?: current_time('timestamp'),
874 ]);
875 update_option('yatra_license', $license);
876 Logger::info('Migrated legacy Pro license options into unified yatra_license store.', [
877 'source' => 'migration',
878 'slug' => $pluginSlug,
879 'status' => $license[$pluginSlug]['status'] ?? '',
880 'has_key' => !empty($license[$pluginSlug]['license_key']),
881 ]);
882 }
883
884 // Keep legacy yatra_pro_* options populated for admin UI reads.
885 if ($legacyKey !== '' && ($this->isForceMigration() || get_option('yatra_pro_license_key', '') === '')) {
886 update_option('yatra_pro_license_key', $legacyKey);
887 }
888 if ($legacyStatus !== '' && ($this->isForceMigration() || get_option('yatra_pro_license_status', '') === '')) {
889 update_option('yatra_pro_license_status', $legacyStatus);
890 }
891 if ($legacyExpires !== '' && ($this->isForceMigration() || get_option('yatra_pro_license_expires', '') === '')) {
892 update_option('yatra_pro_license_expires', $legacyExpires);
893 }
894 }
895
896 /**
897 * Migrate payment gateway configurations from old Yatra 2.x.
898 *
899 * Old Yatra stored each gateway's credentials as individual flat wp_options:
900 *
901 * PayPal (core plugin):
902 * yatra_payment_gateway_paypal_email
903 * yatra_payment_gateway_paypal_label_on_checkout
904 *
905 * Stripe (yatra-stripe add-on):
906 * yatra_payment_gateway_stripe_live_publishable_key
907 * yatra_payment_gateway_stripe_live_secret_key
908 * yatra_payment_gateway_stripe_test_publishable_key
909 * yatra_payment_gateway_stripe_test_secret_key
910 * yatra_payment_gateway_stripe_webhook_endpoint_secret
911 * yatra_payment_gateway_stripe_label_on_checkout
912 *
913 * Razorpay (yatra-razorpay add-on):
914 * yatra_payment_gateway_razorpay_key_id
915 * yatra_payment_gateway_razorpay_key_secret
916 * yatra_payment_gateway_razorpay_payment_action
917 * yatra_payment_gateway_razorpay_enable_webhook
918 * yatra_payment_gateway_razorpay_webhook_secret
919 * yatra_payment_gateway_razorpay_label_on_checkout
920 *
921 * 2Checkout (yatra-2checkout add-on):
922 * yatra_payment_gateway_2checkout_live_publishable_key
923 * yatra_payment_gateway_2checkout_live_private_key
924 * yatra_payment_gateway_2checkout_merchant_code
925 * yatra_payment_gateway_2checkout_ins_secret_word
926 * yatra_payment_gateway_2checkout_webhook_endpoint_secret
927 * yatra_payment_gateway_2checkout_label_on_checkout
928 *
929 * Booking Only (core plugin):
930 * yatra_payment_gateway_booking_only_label_on_checkout
931 *
932 * New Yatra 3.0 stores all configs in a single serialised option:
933 * yatra_gateway_configs = [
934 * 'paypal' => ['email' => '...', ...],
935 * 'stripe' => ['api_key' => '...', 'api_secret' => '...', ...],
936 * 'razorpay'=> ['key_id' => '...', 'key_secret' => '...', ...],
937 * ...
938 * ]
939 *
940 * Active gateway slugs in old system use 'booking_only'; new system uses 'pay_later'.
941 *
942 * @return array ['migrated' => int, 'skipped' => int]
943 */
944 private function migratePaymentGatewayConfigs(): array
945 {
946 $gatewayConfigs = [];
947 $migrated = 0;
948 $skipped = 0;
949
950 // Global test mode flag from old plugin (applies to all gateways).
951 $globalTestMode = get_option('yatra_payment_gateway_test_mode', 'no') === 'yes';
952
953 // ── PayPal ────────────────────────────────────────────────────────────
954 $paypalEmail = get_option('yatra_payment_gateway_paypal_email', '');
955 if (!empty($paypalEmail)) {
956 $gatewayConfigs['paypal'] = [
957 'email' => sanitize_email($paypalEmail),
958 'test_mode' => $globalTestMode,
959 'title' => get_option('yatra_payment_gateway_paypal_label_on_checkout', 'PayPal Standard'),
960 ];
961 $migrated++;
962 Logger::info('Migrated PayPal gateway config.', [
963 'source' => 'migration',
964 'has_email' => !empty($paypalEmail),
965 ]);
966 } else {
967 $skipped++;
968 Logger::debug('Skipped PayPal gateway config — yatra_payment_gateway_paypal_email not found.', [
969 'source' => 'migration',
970 ]);
971 }
972
973 // ── Stripe ────────────────────────────────────────────────────────────
974 // Keys differ between live and test mode; pick the appropriate set.
975 // Also merge legacy serialized yatra_stripe_settings (some installs stored keys only there).
976 $stripeLegacy = get_option('yatra_stripe_settings', null);
977 if (is_string($stripeLegacy)) {
978 $maybe = maybe_unserialize($stripeLegacy);
979 $stripeLegacy = is_array($maybe) ? $maybe : null;
980 }
981 if (!is_array($stripeLegacy)) {
982 $stripeLegacy = [];
983 }
984
985 $stripeLivePub = (string) ($stripeLegacy['live_publishable_key'] ?? $stripeLegacy['publishable_key'] ?? get_option('yatra_payment_gateway_stripe_live_publishable_key', ''));
986 $stripeLiveSecret = (string) ($stripeLegacy['live_secret_key'] ?? $stripeLegacy['secret_key'] ?? get_option('yatra_payment_gateway_stripe_live_secret_key', ''));
987 $stripeTestPub = (string) ($stripeLegacy['test_publishable_key'] ?? get_option('yatra_payment_gateway_stripe_test_publishable_key', ''));
988 $stripeTestSecret = (string) ($stripeLegacy['test_secret_key'] ?? get_option('yatra_payment_gateway_stripe_test_secret_key', ''));
989
990 $stripePubKey = $globalTestMode ? $stripeTestPub : $stripeLivePub;
991 $stripeSecretKey = $globalTestMode ? $stripeTestSecret : $stripeLiveSecret;
992
993 // Stripe needs BOTH keys. If the mode-selected pair is incomplete, use whichever full pair exists (common: test mode flag wrong vs keys).
994 $pairComplete = $stripePubKey !== '' && $stripeSecretKey !== '';
995 if (!$pairComplete) {
996 if ($stripeLivePub !== '' && $stripeLiveSecret !== '') {
997 $stripePubKey = $stripeLivePub;
998 $stripeSecretKey = $stripeLiveSecret;
999 } elseif ($stripeTestPub !== '' && $stripeTestSecret !== '') {
1000 $stripePubKey = $stripeTestPub;
1001 $stripeSecretKey = $stripeTestSecret;
1002 }
1003 }
1004
1005 $stripeWebhook = (string) ($stripeLegacy['webhook_secret'] ?? $stripeLegacy['webhook_endpoint_secret'] ?? get_option('yatra_payment_gateway_stripe_webhook_endpoint_secret', ''));
1006
1007 // Refresh flat copies for storing all four keys on the unified row.
1008 $stripeLivePub = (string) get_option('yatra_payment_gateway_stripe_live_publishable_key', '') ?: $stripeLivePub;
1009 $stripeLiveSecret = (string) get_option('yatra_payment_gateway_stripe_live_secret_key', '') ?: $stripeLiveSecret;
1010 $stripeTestPub = (string) get_option('yatra_payment_gateway_stripe_test_publishable_key', '') ?: $stripeTestPub;
1011 $stripeTestSecret = (string) get_option('yatra_payment_gateway_stripe_test_secret_key', '') ?: $stripeTestSecret;
1012
1013 if (!empty($stripePubKey) || !empty($stripeSecretKey)) {
1014 $gatewayConfigs['stripe'] = array_filter([
1015 // New Pro gateway uses 'api_key' (publishable) and 'api_secret' (secret).
1016 'api_key' => $stripePubKey,
1017 'api_secret' => $stripeSecretKey,
1018 'webhook_secret' => $stripeWebhook,
1019 'test_mode' => $globalTestMode,
1020 'title' => get_option('yatra_payment_gateway_stripe_label_on_checkout', 'Pay with Credit / Debit Card'),
1021 // Preserve all four keys so switching modes works without re-entry.
1022 'live_publishable_key' => $stripeLivePub,
1023 'live_secret_key' => $stripeLiveSecret,
1024 'test_publishable_key' => $stripeTestPub,
1025 'test_secret_key' => $stripeTestSecret,
1026 ], fn($v) => $v !== '' && $v !== null);
1027 $migrated++;
1028 Logger::info('Migrated Stripe gateway config.', [
1029 'source' => 'migration',
1030 'has_pub_key' => !empty($stripePubKey),
1031 'has_secret_key' => !empty($stripeSecretKey),
1032 ]);
1033 } else {
1034 $skipped++;
1035 Logger::debug('Skipped Stripe gateway config — no keys found.', ['source' => 'migration']);
1036 }
1037
1038 // ── Razorpay ──────────────────────────────────────────────────────────
1039 // Old sites used either generic keys or separate live/test option names (Pro add-ons).
1040 $razorKeyId = (string) get_option('yatra_payment_gateway_razorpay_key_id', '');
1041 $razorKeySecret = (string) get_option('yatra_payment_gateway_razorpay_key_secret', '');
1042 if ($razorKeyId === '' && $razorKeySecret === '') {
1043 $razorKeyId = $globalTestMode
1044 ? (string) get_option('yatra_payment_gateway_razorpay_test_key_id', '')
1045 : (string) get_option('yatra_payment_gateway_razorpay_live_key_id', '');
1046 $razorKeySecret = $globalTestMode
1047 ? (string) get_option('yatra_payment_gateway_razorpay_test_key_secret', '')
1048 : (string) get_option('yatra_payment_gateway_razorpay_live_key_secret', '');
1049 }
1050
1051 if (!empty($razorKeyId) || !empty($razorKeySecret)) {
1052 $gatewayConfigs['razorpay'] = array_filter([
1053 'key_id' => $razorKeyId,
1054 'key_secret' => $razorKeySecret,
1055 'payment_action' => get_option('yatra_payment_gateway_razorpay_payment_action', 'capture'),
1056 'webhook_secret' => get_option('yatra_payment_gateway_razorpay_webhook_secret', ''),
1057 'test_mode' => $globalTestMode,
1058 'title' => get_option('yatra_payment_gateway_razorpay_label_on_checkout', 'Pay with Cards'),
1059 ], fn($v) => $v !== '' && $v !== null);
1060 $migrated++;
1061 Logger::info('Migrated Razorpay gateway config.', [
1062 'source' => 'migration',
1063 'has_key_id' => !empty($razorKeyId),
1064 'has_key_secret'=> !empty($razorKeySecret),
1065 ]);
1066 } else {
1067 $skipped++;
1068 Logger::debug('Skipped Razorpay gateway config — no keys found.', ['source' => 'migration']);
1069 }
1070
1071 // ── 2Checkout ─────────────────────────────────────────────────────────
1072 $twoCheckoutPubKey = get_option('yatra_payment_gateway_2checkout_live_publishable_key', '');
1073 $twoCheckoutPrivateKey = get_option('yatra_payment_gateway_2checkout_live_private_key', '');
1074 $twoCheckoutMerchant = get_option('yatra_payment_gateway_2checkout_merchant_code', '');
1075
1076 if (!empty($twoCheckoutPubKey) || !empty($twoCheckoutPrivateKey) || !empty($twoCheckoutMerchant)) {
1077 $gatewayConfigs['2checkout'] = array_filter([
1078 'publishable_key' => $twoCheckoutPubKey,
1079 'private_key' => $twoCheckoutPrivateKey,
1080 'merchant_code' => $twoCheckoutMerchant,
1081 'ins_secret_word' => get_option('yatra_payment_gateway_2checkout_ins_secret_word', ''),
1082 'webhook_secret' => get_option('yatra_payment_gateway_2checkout_webhook_endpoint_secret', ''),
1083 'test_mode' => $globalTestMode,
1084 'title' => get_option('yatra_payment_gateway_2checkout_label_on_checkout', 'Pay with Cards'),
1085 ], fn($v) => $v !== '' && $v !== null);
1086 $migrated++;
1087 Logger::info('Migrated 2Checkout gateway config.', ['source' => 'migration']);
1088 } else {
1089 $skipped++;
1090 Logger::debug('Skipped 2Checkout gateway config — no keys found.', ['source' => 'migration']);
1091 }
1092
1093 // ── Mollie (legacy flat wp_options from Yatra 2.x / Pro gateway pack) ──
1094 $mollieKey = $globalTestMode
1095 ? (string) get_option('yatra_payment_gateway_mollie_test_api_key', '')
1096 : (string) get_option('yatra_payment_gateway_mollie_live_api_key', '');
1097 if ($mollieKey === '') {
1098 $mollieKey = (string) get_option('yatra_payment_gateway_mollie_live_api_key', '');
1099 if ($mollieKey === '') {
1100 $mollieKey = (string) get_option('yatra_payment_gateway_mollie_test_api_key', '');
1101 }
1102 }
1103 if ($mollieKey !== '') {
1104 $gatewayConfigs['mollie'] = array_filter([
1105 'api_key' => $mollieKey,
1106 'webhook_secret' => (string) get_option('yatra_payment_gateway_mollie_webhook_secret', ''),
1107 'test_mode' => $globalTestMode,
1108 'title' => get_option('yatra_payment_gateway_mollie_label_on_checkout', 'Pay with Mollie'),
1109 ], fn($v) => $v !== '' && $v !== null);
1110 $migrated++;
1111 Logger::info('Migrated Mollie gateway config.', ['source' => 'migration']);
1112 } else {
1113 $skipped++;
1114 Logger::debug('Skipped Mollie gateway config — no API key found.', ['source' => 'migration']);
1115 }
1116
1117 // ── Square (flat keys; yatra_pro_square_settings merged later in mergeLegacyProBundledGatewayOptions)
1118 $squareAppId = $globalTestMode
1119 ? (string) get_option('yatra_payment_gateway_square_test_application_id', '')
1120 : (string) get_option('yatra_payment_gateway_square_live_application_id', '');
1121 $squareToken = $globalTestMode
1122 ? (string) get_option('yatra_payment_gateway_square_test_access_token', '')
1123 : (string) get_option('yatra_payment_gateway_square_live_access_token', '');
1124 $squareLoc = $globalTestMode
1125 ? (string) get_option('yatra_payment_gateway_square_test_location_id', '')
1126 : (string) get_option('yatra_payment_gateway_square_live_location_id', '');
1127 if ($squareAppId === '' && $squareToken === '') {
1128 $squareAppId = (string) get_option('yatra_payment_gateway_square_live_application_id', '');
1129 $squareToken = (string) get_option('yatra_payment_gateway_square_live_access_token', '');
1130 $squareLoc = (string) get_option('yatra_payment_gateway_square_live_location_id', '');
1131 }
1132 if ($squareAppId !== '' || $squareToken !== '' || $squareLoc !== '') {
1133 $gatewayConfigs['square'] = array_filter([
1134 'application_id' => $squareAppId,
1135 'access_token' => $squareToken,
1136 'location_id' => $squareLoc,
1137 'test_mode' => $globalTestMode,
1138 'title' => get_option('yatra_payment_gateway_square_label_on_checkout', 'Pay With Card'),
1139 ], fn($v) => $v !== '' && $v !== null && $v !== false);
1140 $migrated++;
1141 Logger::info('Migrated Square gateway config (flat legacy options).', ['source' => 'migration']);
1142 } else {
1143 $skipped++;
1144 Logger::debug('Skipped Square flat gateway config — no keys found.', ['source' => 'migration']);
1145 }
1146
1147 // ── Authorize.Net (flat keys + optional yatra_pro_authorizenet_settings later)
1148 $authLogin = $globalTestMode
1149 ? (string) get_option('yatra_payment_gateway_authorizenet_test_login_id', '')
1150 : (string) get_option('yatra_payment_gateway_authorizenet_live_login_id', '');
1151 $authTxn = $globalTestMode
1152 ? (string) get_option('yatra_payment_gateway_authorizenet_test_transaction_key', '')
1153 : (string) get_option('yatra_payment_gateway_authorizenet_live_transaction_key', '');
1154 $authPub = (string) get_option('yatra_payment_gateway_authorizenet_public_client_key', '');
1155 if ($authPub === '') {
1156 $authPub = (string) get_option('yatra_payment_gateway_authorizenet_client_key', '');
1157 }
1158 if ($authLogin === '' && $authTxn === '') {
1159 $authLogin = (string) get_option('yatra_payment_gateway_authorizenet_live_login_id', '');
1160 $authTxn = (string) get_option('yatra_payment_gateway_authorizenet_live_transaction_key', '');
1161 }
1162 if ($authLogin !== '' || $authTxn !== '' || $authPub !== '') {
1163 $gatewayConfigs['authorize_net'] = array_filter([
1164 'api_login_id' => $authLogin,
1165 'transaction_key' => $authTxn,
1166 'public_client_key' => $authPub,
1167 'test_mode' => $globalTestMode,
1168 'title' => get_option('yatra_payment_gateway_authorizenet_label_on_checkout', 'Pay with Cards'),
1169 ], fn($v) => $v !== '' && $v !== null);
1170 $migrated++;
1171 Logger::info('Migrated Authorize.Net gateway config (flat legacy options).', ['source' => 'migration']);
1172 } else {
1173 $skipped++;
1174 Logger::debug('Skipped Authorize.Net flat gateway config — no keys found.', ['source' => 'migration']);
1175 }
1176
1177 // ── Booking-Only / Pay-Later ──────────────────────────────────────────
1178 // Old slug was 'booking_only'; new system calls it 'pay_later'.
1179 $bookingOnlyLabel = get_option('yatra_payment_gateway_booking_only_label_on_checkout', 'Book Now Pay Later');
1180 $gatewayConfigs['pay_later'] = [
1181 'title' => $bookingOnlyLabel,
1182 ];
1183 $migrated++;
1184 Logger::info('Migrated Pay Later (booking_only) gateway config.', ['source' => 'migration']);
1185
1186 // ── Persist all configs ───────────────────────────────────────────────
1187 if (!empty($gatewayConfigs)) {
1188 // Merge with any existing pro-gateway configs already saved so we do
1189 // not clobber configs written by Pro module registration.
1190 $existing = get_option('yatra_gateway_configs', []);
1191 if (!is_array($existing)) {
1192 $existing = [];
1193 }
1194 $merged = $this->mergeGatewayConfigs($existing, $gatewayConfigs, $this->isForceMigration());
1195 // Enable gateways when we have credentials/config for them.
1196 $merged = $this->applyGatewayEnabledFlagsFromSlugs($merged, array_keys($gatewayConfigs));
1197 update_option('yatra_gateway_configs', $merged);
1198 Logger::info('Saved yatra_gateway_configs.', [
1199 'source' => 'migration',
1200 'count' => count($merged),
1201 'gateways' => array_keys($merged),
1202 ]);
1203 $this->ensurePaymentGatewaySlugsForConfigs($merged);
1204 }
1205
1206 // ── Fix active-gateway slug: booking_only → pay_later ─────────────────
1207 $activeGateways = get_option('yatra_payment_gateways', []);
1208 if (is_array($activeGateways) && in_array('booking_only', $activeGateways, true)) {
1209 $activeGateways = array_map(
1210 static fn($slug) => $slug === 'booking_only' ? 'pay_later' : $slug,
1211 $activeGateways
1212 );
1213 update_option('yatra_payment_gateways', array_values(array_unique($activeGateways)));
1214 Logger::info('Renamed booking_only → pay_later in payment gateways list.', ['source' => 'migration']);
1215 }
1216
1217 // Legacy Yatra Pro 2.x bundled gateway settings (yatra_pro_* options). Runs after free flat-key
1218 // migration so yatra_gateway_configs from core/free wins on duplicate keys; Pro only fills gaps.
1219 $this->mergeLegacyProBundledGatewayOptions();
1220
1221 return [
1222 'migrated' => $migrated,
1223 'skipped' => $skipped,
1224 ];
1225 }
1226
1227 /**
1228 * Merge legacy Pro payment gateway options into yatra_payment_gateways and yatra_gateway_configs.
1229 *
1230 * Free/core migration above already maps yatra_payment_gateways + yatra_payment_gateway_* flat keys.
1231 * This handles Pro-only bundles (yatra_pro_*_settings) without a second migration step.
1232 */
1233 private function mergeLegacyProBundledGatewayOptions(): void
1234 {
1235 $rawLegacyEnabled = get_option('yatra_pro_enabled_payment_gateways', []);
1236 $rawLegacyEnabled = is_array($rawLegacyEnabled) ? $rawLegacyEnabled : [];
1237
1238 $legacySettings = [
1239 '2checkout' => get_option('yatra_pro_twocheckout_settings', null),
1240 'square' => get_option('yatra_pro_square_settings', null),
1241 'razorpay' => get_option('yatra_pro_razorpay_settings', null),
1242 'authorize_net' => get_option('yatra_pro_authorizenet_settings', null),
1243 ];
1244
1245 $proFeatures = get_option('yatra_pro_features', []);
1246 $proFeatures = is_array($proFeatures) ? $proFeatures : [];
1247 $paymentFeatureOn = !empty($proFeatures['payment_gateways']);
1248
1249 $hasBundledSettings = false;
1250 foreach ($legacySettings as $v) {
1251 if ($v !== null && $v !== '' && $v !== []) {
1252 $hasBundledSettings = true;
1253 break;
1254 }
1255 }
1256
1257 // Old Pro often left yatra_pro_enabled_payment_gateways empty while get_enabled_gateways() defaulted
1258 // to all gateways at runtime — infer from core yatra_payment_gateways + Pro feature toggle.
1259 $legacyEnabled = $this->buildLegacyProEnabledGatewayList($rawLegacyEnabled, $paymentFeatureOn);
1260
1261 $hasAny = $legacyEnabled !== []
1262 || $hasBundledSettings
1263 || $paymentFeatureOn;
1264 if (!$hasAny) {
1265 return;
1266 }
1267
1268 $active = get_option('yatra_payment_gateways', []);
1269 if (!is_array($active)) {
1270 $active = [];
1271 }
1272
1273 $enabledMapped = [];
1274 foreach ($legacyEnabled as $g) {
1275 $g = strtolower(trim((string) $g));
1276 if ($g === '') {
1277 continue;
1278 }
1279 $map = [
1280 'booking_only' => 'pay_later',
1281 'authorizenet' => 'authorize_net',
1282 'authorize' => 'authorize_net',
1283 'two_checkout' => '2checkout',
1284 'twocheckout' => '2checkout',
1285 ];
1286 $enabledMapped[] = $map[$g] ?? $g;
1287 }
1288 $enabledMapped = array_values(array_filter($enabledMapped, static fn ($v) => $v !== ''));
1289
1290 // Free/core list first so ordering matches the main migration; Pro appends any missing slugs.
1291 $finalActive = array_values(array_unique(array_merge($active, $enabledMapped)));
1292 update_option('yatra_payment_gateways', $finalActive);
1293
1294 $configs = get_option('yatra_gateway_configs', []);
1295 if (!is_array($configs)) {
1296 $configs = [];
1297 }
1298
1299 foreach ($legacySettings as $gatewayId => $settings) {
1300 if (!is_array($settings) || $settings === []) {
1301 continue;
1302 }
1303
1304 $settings = $this->mapLegacyProBundledGatewaySettingsToUnifiedConfigs($gatewayId, $settings);
1305
1306 if ($gatewayId === 'authorize_net' && isset($settings['client_key']) && !isset($settings['public_client_key'])) {
1307 $settings['public_client_key'] = $settings['client_key'];
1308 }
1309
1310 $configs[$gatewayId] = $this->mergeGatewayConfigRow(
1311 is_array($configs[$gatewayId] ?? null) ? $configs[$gatewayId] : [],
1312 $settings,
1313 $this->isForceMigration()
1314 );
1315 }
1316
1317 $configs = $this->applyGatewayEnabledFlagsFromSlugs($configs, $finalActive);
1318 update_option('yatra_gateway_configs', $configs);
1319
1320 $this->ensurePaymentGatewaySlugsForConfigs($configs);
1321
1322 Logger::info('Merged legacy Pro bundled gateway options into unified gateway settings.', [
1323 'source' => 'migration',
1324 'active_gateways' => $finalActive,
1325 'config_keys' => array_keys($configs),
1326 ]);
1327 }
1328
1329 /**
1330 * Rebuild the effective list of Pro 2.x "enabled" gateway IDs for migration.
1331 *
1332 * @param array<int|string, mixed> $storedOption Value of yatra_pro_enabled_payment_gateways
1333 */
1334 private function buildLegacyProEnabledGatewayList(array $storedOption, bool $paymentFeatureOn): array
1335 {
1336 $slugMap = [
1337 'booking_only' => 'pay_later',
1338 'authorizenet' => 'authorize_net',
1339 'authorize' => 'authorize_net',
1340 'two_checkout' => '2checkout',
1341 'twocheckout' => '2checkout',
1342 ];
1343
1344 $normalizeSlug = static function (string $s) use ($slugMap): string {
1345 $s = strtolower(trim($s));
1346
1347 return $slugMap[$s] ?? $s;
1348 };
1349
1350 $fromStored = [];
1351 foreach ($storedOption as $g) {
1352 $g = strtolower(trim((string) $g));
1353 if ($g === '') {
1354 continue;
1355 }
1356 $fromStored[] = $normalizeSlug($g);
1357 }
1358 $fromStored = array_values(array_unique(array_filter($fromStored)));
1359
1360 $fromCore = [];
1361 $core = get_option('yatra_payment_gateways', []);
1362 if (is_array($core) && $core !== []) {
1363 foreach ($core as $key => $val) {
1364 $on = $val === 'yes' || $val === true || $val === 1 || $val === '1';
1365 if (!$on) {
1366 continue;
1367 }
1368 if (is_int($key)) {
1369 $fromCore[] = $normalizeSlug((string) $val);
1370 } else {
1371 $fromCore[] = $normalizeSlug((string) $key);
1372 }
1373 }
1374 }
1375 $fromCore = array_values(array_unique(array_filter($fromCore)));
1376
1377 // Pro gateways only (core free PayPal etc. are handled by migratePaymentGatewayConfigs).
1378 $proSlugs = ['stripe', 'square', 'razorpay', 'authorize_net', '2checkout', 'mollie'];
1379 $filterPro = static function (array $slugs) use ($proSlugs): array {
1380 return array_values(array_intersect($slugs, $proSlugs));
1381 };
1382
1383 $merged = array_values(array_unique(array_merge($fromStored, $filterPro($fromCore))));
1384
1385 // Match old PaymentGateways::get_enabled_gateways() default when the option was never persisted.
1386 if ($merged === [] && $paymentFeatureOn) {
1387 return ['stripe', 'square', 'razorpay', 'authorize_net', '2checkout', 'mollie'];
1388 }
1389
1390 return $merged;
1391 }
1392
1393 /**
1394 * If unified configs contain real credentials but the gateway slug is missing from the active list
1395 * (common when legacy sites only stored flat keys, not yatra_payment_gateways), append the slug.
1396 *
1397 * @param array<string, array<string, mixed>> $merged
1398 */
1399 private function ensurePaymentGatewaySlugsForConfigs(array $merged): void
1400 {
1401 $active = get_option('yatra_payment_gateways', []);
1402 if (!is_array($active)) {
1403 $active = [];
1404 }
1405
1406 $rules = [
1407 'paypal' => static fn (array $c): bool => !empty($c['email']),
1408 'stripe' => static fn (array $c): bool => !empty($c['api_key']) || !empty($c['api_secret']),
1409 'razorpay' => static fn (array $c): bool => !empty($c['key_id']) || !empty($c['key_secret']),
1410 '2checkout' => static fn (array $c): bool => !empty($c['publishable_key']) || !empty($c['merchant_code']) || !empty($c['private_key']),
1411 'mollie' => static fn (array $c): bool => !empty($c['api_key']),
1412 'square' => static fn (array $c): bool => !empty($c['application_id']) || !empty($c['access_token']),
1413 'authorize_net' => static fn (array $c): bool => !empty($c['api_login_id']) || !empty($c['transaction_key']),
1414 ];
1415
1416 foreach ($rules as $slug => $hasCreds) {
1417 if (empty($merged[$slug]) || !is_array($merged[$slug])) {
1418 continue;
1419 }
1420 if (!$hasCreds($merged[$slug])) {
1421 continue;
1422 }
1423 if (!in_array($slug, $active, true)) {
1424 $active[] = $slug;
1425 }
1426 }
1427
1428 update_option('yatra_payment_gateways', array_values(array_unique($active)));
1429 }
1430
1431 /**
1432 * Ensure configs reflect enabled gateways list (sets config['enabled']=true for those slugs).
1433 *
1434 * @param array<string, mixed> $configs
1435 * @param array<int, string> $enabledSlugs
1436 * @return array<string, mixed>
1437 */
1438 private function applyGatewayEnabledFlagsFromSlugs(array $configs, array $enabledSlugs): array
1439 {
1440 $enabled = array_values(array_unique(array_map(
1441 static fn ($s) => strtolower(trim((string) $s)),
1442 $enabledSlugs
1443 )));
1444
1445 $map = [
1446 'booking_only' => 'pay_later',
1447 'authorizenet' => 'authorize_net',
1448 'authorize' => 'authorize_net',
1449 'two_checkout' => '2checkout',
1450 'twocheckout' => '2checkout',
1451 ];
1452 $enabled = array_map(static fn ($s) => $map[$s] ?? $s, $enabled);
1453
1454 foreach ($enabled as $slug) {
1455 if ($slug === '') {
1456 continue;
1457 }
1458 if (!isset($configs[$slug]) || !is_array($configs[$slug])) {
1459 $configs[$slug] = [];
1460 }
1461 $configs[$slug]['enabled'] = true;
1462 }
1463
1464 return $configs;
1465 }
1466
1467 /**
1468 * Merge full gateway configs map.
1469 *
1470 * Non-force mode: do NOT overwrite non-empty existing values (installer creates empty placeholders).
1471 * Force mode: incoming values overwrite existing.
1472 *
1473 * @param array<string, mixed> $existing
1474 * @param array<string, mixed> $incoming
1475 * @return array<string, mixed>
1476 */
1477 private function mergeGatewayConfigs(array $existing, array $incoming, bool $force): array
1478 {
1479 $merged = $existing;
1480
1481 foreach ($incoming as $gatewayId => $row) {
1482 if (!is_array($row)) {
1483 continue;
1484 }
1485 $mergedRow = is_array($merged[$gatewayId] ?? null) ? $merged[$gatewayId] : [];
1486 $merged[$gatewayId] = $this->mergeGatewayConfigRow($mergedRow, $row, $force);
1487 }
1488
1489 return $merged;
1490 }
1491
1492 /**
1493 * Merge a single gateway config row.
1494 *
1495 * @param array<string, mixed> $existing
1496 * @param array<string, mixed> $incoming
1497 * @return array<string, mixed>
1498 */
1499 private function mergeGatewayConfigRow(array $existing, array $incoming, bool $force): array
1500 {
1501 if ($force) {
1502 // Force: incoming wins, but keep any extra existing keys not present in incoming.
1503 return array_merge($existing, $incoming);
1504 }
1505
1506 // Non-force: only fill missing/empty values.
1507 $out = $existing;
1508 foreach ($incoming as $k => $v) {
1509 $hasExisting = array_key_exists($k, $existing);
1510 $existingVal = $hasExisting ? $existing[$k] : null;
1511
1512 $existingEmpty = ($existingVal === null)
1513 || ($existingVal === '')
1514 || ($existingVal === [])
1515 || ($existingVal === false);
1516
1517 if (!$hasExisting || $existingEmpty) {
1518 $out[$k] = $v;
1519 }
1520 }
1521
1522 return $out;
1523 }
1524
1525 /**
1526 * Legacy Pro stored gateway settings in separate option arrays (yatra_pro_*_settings) with keys that
1527 * don't always match the unified Yatra 3.x gateway config schema.
1528 *
1529 * This normalizes those arrays to the keys the new gateway classes actually read.
1530 *
1531 * @param array<string, mixed> $settings
1532 * @return array<string, mixed>
1533 */
1534 private function mapLegacyProBundledGatewaySettingsToUnifiedConfigs(string $gatewayId, array $settings): array
1535 {
1536 $out = $settings;
1537
1538 // Normalize enable flag to boolean; unified configs use config['enabled'] bool.
1539 if (isset($out['enabled'])) {
1540 $enabled = $out['enabled'];
1541 $out['enabled'] = ($enabled === 'yes' || $enabled === true || $enabled === 1 || $enabled === '1' || $enabled === 'on');
1542 }
1543
1544 // Normalize common test mode flags; unified system uses global yatra_payment_test_mode primarily,
1545 // but we preserve per-gateway flag when present for backward compatibility.
1546 if (isset($out['test_mode'])) {
1547 $tm = $out['test_mode'];
1548 $out['test_mode'] = ($tm === 'yes' || $tm === true || $tm === 1 || $tm === '1' || $tm === 'on');
1549 }
1550
1551 // Gateway-specific key mapping
1552 switch ($gatewayId) {
1553 case '2checkout':
1554 // Legacy Pro: seller_id, secret_word
1555 // Unified configs (migration flat keys): merchant_code, ins_secret_word
1556 if (!isset($out['merchant_code']) && isset($out['seller_id'])) {
1557 $out['merchant_code'] = (string) $out['seller_id'];
1558 }
1559 if (!isset($out['ins_secret_word']) && isset($out['secret_word'])) {
1560 $out['ins_secret_word'] = (string) $out['secret_word'];
1561 }
1562 break;
1563
1564 case 'authorize_net':
1565 // Legacy Pro array sometimes used signature_key as the public client key; in new gateway it is public_client_key.
1566 if (!isset($out['public_client_key']) && isset($out['signature_key'])) {
1567 $out['public_client_key'] = (string) $out['signature_key'];
1568 }
1569 break;
1570 }
1571
1572 return $out;
1573 }
1574
1575 /**
1576 * Map legacy Pro review + partial-payment options into Yatra 3.x SettingsService option keys.
1577 * Only writes when the legacy option exists so fresh 3.x installs are untouched.
1578 */
1579 private function migrateLegacyReviewAndPartialPaymentOptions(): void
1580 {
1581 $revEnable = get_option('yatra_review_enable', null);
1582 if ($revEnable !== null && $revEnable !== '') {
1583 $on = $revEnable === 'yes' || $revEnable === true || $revEnable === 1 || $revEnable === '1';
1584 update_option('yatra_enable_reviews', $on);
1585 Logger::info('Migrated legacy yatra_review_enable → yatra_enable_reviews', ['source' => 'migration']);
1586 }
1587
1588 $who = get_option('yatra_review_who_can', null);
1589 if (is_string($who) && $who !== '') {
1590 $w = strtolower($who);
1591 if (str_contains($w, 'book')) {
1592 update_option('yatra_require_booking_to_review', true);
1593 } elseif ($w === 'logged_in' || str_contains($w, 'login')) {
1594 update_option('yatra_require_booking_to_review', false);
1595 }
1596 Logger::info('Migrated legacy yatra_review_who_can', ['source' => 'migration', 'value' => $who]);
1597 }
1598
1599 $auto = get_option('yatra_review_autopublish', null);
1600 if ($auto !== null && $auto !== '') {
1601 $publish = strtolower((string) $auto) === 'publish';
1602 update_option('yatra_auto_approve_reviews', $publish);
1603 update_option('yatra_enable_review_moderation', !$publish);
1604 Logger::info('Migrated legacy yatra_review_autopublish', ['source' => 'migration', 'value' => $auto]);
1605 }
1606
1607 $partial = get_option('yatra_enable_partial_payment', null);
1608 if ($partial === 'yes' || $partial === true || $partial === 1 || $partial === '1') {
1609 update_option('yatra_partial_payment', true);
1610 $pct = (float) get_option('yatra_first_installment_payment', 0);
1611 $type = (string) get_option('yatra_first_installment_payment_type', 'percentage');
1612 if ($type === 'percentage' && $pct > 0 && $pct < 100) {
1613 update_option('yatra_partial_payment_percentage', max(1, min(99, (int) round($pct))));
1614 }
1615
1616 // Pro 3.x: partial payment lives inside Flexible Payments module.
1617 // Enable module + persist Pro settings so UI/runtime behaves like legacy.
1618 $pro = ProMigrationReadiness::getState();
1619 if ($pro['ready']) {
1620 // Free plugin module flag (controls whether Pro module loads at all).
1621 // This is what both Pro's ModuleManager::shouldLoadModule() and admin UI checks rely on.
1622 if (class_exists('\\Yatra\\Core\\Modules\\ModuleManager')) {
1623 // Triggers yatra_module_active hook, which Pro listens to for module activation side-effects.
1624 \Yatra\Core\Modules\ModuleManager::setModuleStatus('flexible_payments', true);
1625 } else {
1626 // Fallback: set raw option if module manager isn't loaded for some reason.
1627 $mods = get_option('yatra_modules', []);
1628 $mods = is_array($mods) ? $mods : [];
1629 $mods['flexible_payments'] = array_merge($mods['flexible_payments'] ?? [], [
1630 'enabled' => true,
1631 'updated_at' => current_time('mysql'),
1632 ]);
1633 update_option('yatra_modules', $mods);
1634 }
1635
1636 $modules = get_option('yatra_pro_modules_enabled', []);
1637 $modules = is_array($modules) ? $modules : [];
1638 if (!in_array('flexible_payments', $modules, true)) {
1639 $modules[] = 'flexible_payments';
1640 update_option('yatra_pro_modules_enabled', array_values(array_unique($modules)));
1641 }
1642
1643 $proFlex = get_option('yatra_pro_flexible_payments', []);
1644 $proFlex = is_array($proFlex) ? $proFlex : [];
1645 $proFlex['partial_payment'] = true;
1646 if ($type === 'percentage' && $pct > 0 && $pct < 100) {
1647 $proFlex['partial_payment_percentage'] = max(1, min(99, (int) round($pct)));
1648 }
1649 // Legacy didn't use deposit as "partial payment" — keep deposit disabled unless user had it separately.
1650 if (!array_key_exists('enable_deposit', $proFlex)) {
1651 $proFlex['enable_deposit'] = false;
1652 }
1653 update_option('yatra_pro_flexible_payments', $proFlex);
1654 }
1655
1656 Logger::info('Migrated legacy partial payment options → yatra_partial_payment*', ['source' => 'migration']);
1657 }
1658 }
1659
1660 /**
1661 * Migrate enquiry and booking form settings
1662 *
1663 * @return array ['migrated' => int, 'skipped' => int]
1664 */
1665 private function migrateEnquirySettings(): array
1666 {
1667 $migrated = 0;
1668 $skipped = 0;
1669
1670 // Check if enquiry forms were enabled in old version
1671 $enquiryEnabled = get_option('yatra_enquiry_form_show', 'no');
1672 if ($enquiryEnabled === 'yes' || $enquiryEnabled === true) {
1673 update_option('yatra_enable_enquiry', true);
1674 $migrated++;
1675 Logger::info("Migrated enquiry form setting", ['source' => 'migration']);
1676 } else {
1677 $skipped++;
1678 Logger::debug("Skipped enquiry form setting (disabled or not found)", ['source' => 'migration']);
1679 }
1680
1681 // Migrate booking form field settings if they exist
1682 $oldBookingFields = get_option('yatra_booking_form_fields', []);
1683 if (!empty($oldBookingFields) && is_array($oldBookingFields)) {
1684 // Transform old booking form fields to new format
1685 update_option('yatra_legacy_booking_fields', $oldBookingFields);
1686 $migrated++;
1687 Logger::info("Migrated booking form fields", ['source' => 'migration', 'count' => count($oldBookingFields)]);
1688 } else {
1689 $skipped++;
1690 Logger::debug("Skipped booking form fields (not found or empty)", ['source' => 'migration']);
1691 }
1692
1693 return [
1694 'migrated' => $migrated,
1695 'skipped' => $skipped,
1696 ];
1697 }
1698 }
1699