PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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 / Services / InstallerService.php

InstallerService.php in Yatra – Travel Booking & Tour Operator Software 3.0.6, at app/Services/InstallerService.php

675 lines 29.0 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\Services;
6
7 /**
8 * Installer Service
9 *
10 * Handles plugin installation and default settings setup
11 * Ensures proper default configuration on fresh installation
12 */
13 class InstallerService
14 {
15 /**
16 * Run installation tasks
17 *
18 * @return void
19 */
20 public static function install(): void
21 {
22 // Create all database tables (one-time action)
23 self::createDatabaseTables();
24
25 // Set all default options for fresh installation
26 self::setDefaultOptions();
27 }
28
29 /**
30 * Create all database tables (centralized table creation)
31 *
32 * @return void
33 */
34 public static function createDatabaseTables(): void
35 {
36 if (class_exists('\Yatra\Core\Database')) {
37 \Yatra\Core\Database::createTables();
38 }
39
40 }
41
42 /**
43 * Set all default options for fresh installation
44 * Only Book Now Pay Later should be enabled by default
45 * Uses SettingsService keys to ensure consistency
46 *
47 * @return void
48 */
49 private static function setDefaultOptions(): void
50 {
51 // Payment Gateway Settings - Only enable Pay Later by default
52 // These match SettingsService defaults exactly
53 update_option('yatra_payment_gateways', ['pay_later']);
54 update_option('yatra_payment_methods', []);
55 update_option('yatra_payment_test_mode', true);
56 update_option('yatra_auto_confirm_pay_later', true);
57 update_option('yatra_partial_payment', false);
58 // Set gateway configs with proper structure - only enable pay_later by default
59 $gateway_configs = [
60 'pay_later' => [
61 'enabled' => true,
62 'title' => 'Book Now, Pay Later',
63 'description' => 'Allow customers to reserve now and pay before the trip',
64 ],
65 // Explicitly disable all other gateways
66 'stripe' => [
67 'enabled' => false,
68 'title' => 'Stripe',
69 'description' => 'Accept credit and debit cards',
70 'api_key' => '',
71 'api_secret' => '',
72 'webhook_secret' => '',
73 ],
74 'paypal' => [
75 'enabled' => false,
76 'title' => 'PayPal',
77 'description' => 'Accept PayPal payments',
78 'api_key' => '',
79 'api_secret' => '',
80 ],
81 'razorpay' => [
82 'enabled' => false,
83 'title' => 'Razorpay',
84 'description' => 'Accept payments via Razorpay',
85 'api_key' => '',
86 'api_secret' => '',
87 ],
88 'square' => [
89 'enabled' => false,
90 'title' => 'Square',
91 'description' => 'Accept payments via Square',
92 'api_key' => '',
93 'api_secret' => '',
94 ],
95 'authorize_net' => [
96 'enabled' => false,
97 'title' => 'Authorize.net',
98 'description' => 'Accept payments via Authorize.net',
99 'api_key' => '',
100 'api_secret' => '',
101 ],
102 'bank_transfer' => [
103 'enabled' => false,
104 'title' => 'Bank Transfer',
105 'description' => 'Accept manual bank transfer payments',
106 'api_key' => '',
107 'api_secret' => '',
108 ]
109 ];
110 update_option('yatra_gateway_configs', $gateway_configs);
111 update_option('yatra_gateway_order', []);
112
113 // Currency Settings - Match SettingsService defaults
114 update_option('yatra_currency', 'USD');
115 update_option('yatra_currency_position', 'before');
116 update_option('yatra_thousand_separator', ',');
117 update_option('yatra_decimal_separator', '.');
118 update_option('yatra_decimal_places', 2);
119
120 // Flexible Payment Settings - Match SettingsService defaults
121 update_option('yatra_enable_deposit', false);
122 update_option('yatra_deposit_type', 'percentage');
123 update_option('yatra_deposit_amount', 20);
124 update_option('yatra_deposit_required', false);
125 update_option('yatra_deposit_percentage', 20);
126 update_option('yatra_partial_payment_percentage', 30);
127
128 update_option('yatra_allow_save_payment_methods', false);
129
130 // Trip Settings - Match SettingsService defaults
131 update_option('yatra_trip_base', 'trip');
132 update_option('yatra_trips_per_page', 12);
133 update_option('yatra_enable_wishlist', false);
134 update_option('yatra_enable_comparison', false);
135 update_option('yatra_show_sold_out', true);
136
137 // Customer Settings - Match SettingsService defaults
138 update_option('yatra_enable_customer_accounts', true);
139 update_option('yatra_enable_customer_registration', true);
140
141 // Booking Settings - Match SettingsService defaults
142 update_option('yatra_booking_base', 'book');
143 update_option('yatra_use_booking_page', false);
144 update_option('yatra_booking_page_id', 0);
145 update_option('yatra_enable_guest_booking', true);
146 update_option('yatra_booking_confirmation', true);
147 update_option('yatra_auto_confirm_bookings', false);
148 update_option('yatra_require_login', false);
149 update_option('yatra_allow_guest_checkout', true);
150 // cancellation_policy / cancellation_days / refund_policy
151 // intentionally not seeded — these are removed settings (see
152 // SettingsController::$default_settings comment). Existing
153 // sites that already have orphan values stored will keep
154 // them in wp_options; new sites won't acquire them.
155 update_option('yatra_booking_expiry_hours', 24);
156 update_option('yatra_booking_reminder_days', 3);
157 update_option('yatra_allow_waitlist', true);
158
159 // Email identity: canonical keys (REST / EmailService) + legacy keys for older code paths
160 $wpAdminEmail = (string) get_option('admin_email', '');
161 $blogName = (string) get_bloginfo('name');
162 update_option('yatra_from_email', $wpAdminEmail);
163 update_option('yatra_from_name', $blogName);
164 update_option('yatra_admin_email', $wpAdminEmail);
165 update_option('yatra_email_from_name', $blogName);
166 update_option('yatra_email_from_address', $wpAdminEmail);
167 update_option('yatra_enable_admin_notifications', true);
168 update_option('yatra_enable_customer_notifications', true);
169
170 // Default transactional template HTML + subjects (Email → Templates / settings API)
171 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $optionKey => $value) {
172 update_option('yatra_' . $optionKey, $value);
173 }
174 update_option('yatra_email_template_booking', true);
175 update_option('yatra_email_template_confirmation', true);
176 update_option('yatra_email_template_cancellation', true);
177 update_option('yatra_email_template_reminder', true);
178 update_option('yatra_email_template_admin_new_booking', true);
179 update_option('yatra_email_template_admin_payment', true);
180 update_option('yatra_email_template_admin_cancellation', true);
181 update_option('yatra_email_template_trip_consent', true);
182 update_option('yatra_email_template_customer_verification', true);
183 update_option('yatra_email_template_guest_verification', true);
184 update_option('yatra_email_template_booking_completed', true);
185 update_option('yatra_email_template_booking_expired_customer', true);
186 update_option('yatra_email_template_admin_booking_expired', true);
187 update_option('yatra_email_template_scheduled_payment_reminder', true);
188 update_option('yatra_email_template_scheduled_payment_succeeded', true);
189 update_option('yatra_email_template_scheduled_payment_failed', true);
190 update_option('yatra_email_template_admin_scheduled_payment_failed', true);
191 update_option('yatra_email_template_enquiry_received', true);
192 update_option('yatra_email_template_enquiry_admin', true);
193 update_option('yatra_email_template_enquiry_response', true);
194 update_option('yatra_email_template_review_request', true);
195 update_option('yatra_email_template_abandoned_booking_recovery_first', true);
196 update_option('yatra_email_template_abandoned_booking_recovery_second', true);
197 update_option('yatra_email_template_abandoned_booking_recovery_final', true);
198
199 // Clear any existing Stripe/PayPal settings that might exist
200 delete_option('yatra_stripe_settings');
201 delete_option('yatra_paypal_settings');
202
203 // Set installation tracking (not in SettingsService but needed for tracking)
204 update_option('yatra_installation_date', current_time('mysql'));
205 update_option('yatra_version', defined('YATRA_VERSION') ? YATRA_VERSION : '3.0.3');
206
207
208 }
209
210 /**
211 * Get all required database tables using Table classes
212 *
213 * @return array
214 */
215 public static function getRequiredTables(): array
216 {
217 // Must match \Yatra\Core\Database::createTables() — used for activation, migrations, and targeted checks.
218 $table_classes = [
219 \Yatra\Database\Tables\TripsTable::class,
220 \Yatra\Database\Tables\BookingsTable::class,
221 \Yatra\Database\Tables\BookingPaymentsTable::class,
222 \Yatra\Database\Tables\CustomersTable::class,
223 \Yatra\Database\Tables\BookingTravellersTable::class,
224 \Yatra\Database\Tables\BookingTravellerMetaTable::class,
225 \Yatra\Database\Tables\BookingDeparturesTable::class,
226 \Yatra\Database\Tables\ReviewsTable::class,
227 \Yatra\Database\Tables\DiscountsTable::class,
228 \Yatra\Database\Tables\EnquiriesTable::class,
229 \Yatra\Database\Tables\TripAvailabilityDatesTable::class,
230 \Yatra\Database\Tables\TripAvailabilityRulesTable::class,
231 \Yatra\Database\Tables\TripRevisionsTable::class,
232 \Yatra\Database\Tables\DeparturesTable::class,
233 \Yatra\Database\Tables\TripItineraryDaysTable::class,
234 \Yatra\Database\Tables\TripItineraryDayEntryTable::class,
235 \Yatra\Database\Tables\ClassificationsTable::class,
236 \Yatra\Database\Tables\TripClassificationsTable::class,
237 \Yatra\Database\Tables\TripContentTable::class,
238 ];
239
240 $table_names = [];
241 foreach ($table_classes as $table_class) {
242 if (class_exists($table_class)) {
243 $table_names[] = $table_class::getTableName();
244 }
245 }
246
247 return $table_names;
248 }
249
250 /**
251 * Whether a prefixed table exists. Uses esc_like() because SQL LIKE treats "_" as a wildcard.
252 */
253 public static function databaseTableExists(string $fullTableName): bool
254 {
255 global $wpdb;
256 if ($fullTableName === '') {
257 return false;
258 }
259 $pattern = $wpdb->esc_like($fullTableName);
260 $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $pattern));
261
262 return $found === $fullTableName;
263 }
264
265 /**
266 * Check if this is a fresh installation
267 *
268 * @return bool
269 */
270 public static function isFreshInstallation(): bool
271 {
272 // Check if Yatra version exists in database
273 $installed_version = get_option('yatra_version');
274
275 // If no version is set, it's a fresh installation
276 if ($installed_version === false) {
277 return true;
278 }
279
280 // Check installation date
281 $installation_date = get_option('yatra_installation_date');
282 if ($installation_date === false) {
283 return true;
284 }
285
286 // Additional check: if core tables don't exist, it's fresh
287 $required_tables = self::getRequiredTables();
288 if (!empty($required_tables)) {
289 $trips_table = $required_tables[0]; // Use first table (already has prefix)
290 if (!self::databaseTableExists($trips_table)) {
291 return true;
292 }
293 }
294
295 return false;
296 }
297
298 /**
299 * One-time: coupon migration incorrectly stored status "active"; 3.x uses "publish" (admin + checkout).
300 */
301 public static function maybeNormalizeMigratedCouponDiscountStatuses(): void
302 {
303 if (get_option('yatra_discount_active_status_normalized_v1')) {
304 return;
305 }
306
307 if (!class_exists('Yatra\\Database\\Tables\\DiscountsTable')) {
308 return;
309 }
310
311 $table = \Yatra\Database\Tables\DiscountsTable::getTableName();
312 if (!self::databaseTableExists($table)) {
313 return;
314 }
315
316 global $wpdb;
317 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
318 $wpdb->query("UPDATE `{$table}` SET `status` = 'publish' WHERE `status` = 'active'");
319
320 update_option('yatra_discount_active_status_normalized_v1', '1', false);
321 }
322
323 /**
324 * One-time normalization of recurring availability rules created against the
325 * legacy schema (only `recurrence_type`, `capacity_value`, `interval`, etc.
326 * were written) so the new admin React UI — which reads `rule_type`,
327 * `seats_total`, `interval_days`, `interval_start_date` — can render and
328 * edit them without showing phantom "1 on All / Active" badges or empty
329 * "Every " patterns.
330 *
331 * What this fixes:
332 * - Sample-data and pre-3.x rows landed with `rule_type` defaulted to
333 * 'weekly' regardless of the actual `recurrence_type`, and with
334 * `seats_total` left NULL (the new capacity column). Daily and monthly
335 * rules therefore appeared as broken weekly rows in the new UI.
336 * - The /counts endpoint correctly reported 1 active rule, but the list
337 * table couldn't render it cleanly, leading users to read the API
338 * response as "ghost data".
339 *
340 * Invariants:
341 * - Idempotent — every UPDATE filters rows whose new columns are still
342 * unset, so re-running is a no-op once the data is healed.
343 * - Read-only on rows already authored by the new UI (`rule_type` already
344 * matches the recurrence intent), so user edits are never overwritten.
345 * - No-ops cleanly when the rules table doesn't exist yet (fresh install
346 * before {@see \Yatra\Core\Database::createTables()} has run).
347 */
348 public static function maybeNormalizeAvailabilityRulesLegacyData(): void
349 {
350 if (get_option('yatra_availability_rules_legacy_normalized_v1')) {
351 return;
352 }
353
354 if (!class_exists('Yatra\\Database\\Tables\\TripAvailabilityRulesTable')) {
355 return;
356 }
357
358 $table = \Yatra\Database\Tables\TripAvailabilityRulesTable::getTableName();
359 if (!self::databaseTableExists($table)) {
360 return;
361 }
362
363 global $wpdb;
364
365 // 1. Daily-recurrence rows whose `rule_type` defaulted to 'weekly':
366 // map to the new "interval" rule type and copy the legacy `interval`
367 // + `start_date` into the new columns the React form binds to.
368 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
369 $wpdb->query("UPDATE `{$table}`
370 SET `rule_type` = 'interval',
371 `interval_days` = COALESCE(`interval_days`, NULLIF(`interval`, 0), 1),
372 `interval_start_date` = COALESCE(`interval_start_date`, `start_date`)
373 WHERE `recurrence_type` = 'daily'
374 AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
375
376 // 2. Monthly-recurrence rows whose `rule_type` defaulted to 'weekly':
377 // relabel to 'monthly'. The new UI uses (week_of_month, day_of_week)
378 // rather than `day_of_month`, so we leave those NULL for the user
379 // to set in the form rather than guess from the legacy day_of_month.
380 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
381 $wpdb->query("UPDATE `{$table}`
382 SET `rule_type` = 'monthly'
383 WHERE `recurrence_type` = 'monthly'
384 AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
385
386 // 3. Weekly-recurrence rows: ensure `rule_type` is set explicitly
387 // (most already match the default; this catches any NULL/empty).
388 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
389 $wpdb->query("UPDATE `{$table}`
390 SET `rule_type` = 'weekly'
391 WHERE `recurrence_type` = 'weekly'
392 AND (`rule_type` IS NULL OR `rule_type` = '')");
393
394 // 4. seats_total backfill from `capacity_value` for fixed-capacity rows
395 // so CapacityService and the React table both surface the right
396 // seat cap without falling through hydrate-time fallbacks.
397 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
398 $wpdb->query("UPDATE `{$table}`
399 SET `seats_total` = `capacity_value`
400 WHERE `seats_total` IS NULL
401 AND `capacity_value` IS NOT NULL
402 AND `capacity_value` > 0
403 AND (`capacity_type` IS NULL OR `capacity_type` = 'fixed')");
404
405 update_option('yatra_availability_rules_legacy_normalized_v1', '1', false);
406 }
407
408 /**
409 * Ensure `wp_yatra_bookings.status` accepts `pending_verification`.
410 *
411 * The 3.0.5 guest email-verification feature introduced a new holding
412 * status (`pending_verification`) but the production ENUM only listed
413 * the legacy values. Until the column is widened, MySQL non-strict
414 * mode silently coerces the value to `''` on insert — which makes
415 * every booking placed with verification enabled appear broken:
416 *
417 * 1. The admin booking list renders an empty status badge (the
418 * React status map has no entry for `''` so the row falls into
419 * the default branch with an empty label).
420 * 2. `BookingService::createBooking` sees the in-memory
421 * `$data['status'] === 'pending_verification'` and defers
422 * firing `yatra_booking_created`, so the customer never gets
423 * the booking-confirmation email.
424 * 3. `verify_email` reads the persisted status (now `''`),
425 * decides the booking is "already verified", skips the deferred
426 * fan-out — so neither the status flip nor the booking email
427 * ever fires.
428 *
429 * Doing the widening here (runIdempotentMaintenance — every admin
430 * pageview) instead of a pure version-gated upgrade step means it
431 * heals installs whose stored yatra_version was already bumped to
432 * 3.0.5 by an earlier failed upgrade attempt. Cheap: one
433 * INFORMATION_SCHEMA query gated by a one-shot option flag, ALTER
434 * runs at most once per install.
435 *
436 * Also backfills any rows whose status was silently coerced to `''`
437 * by the pre-widening insert path: those bookings *should* have
438 * landed in `pending_verification`, so we restore them there. The
439 * original verify-email magic link still works because the HMAC
440 * token is bound to booking_id + email, not status.
441 */
442 public static function maybeAddPendingVerificationBookingStatus(): void
443 {
444 if (get_option('yatra_booking_status_pending_verification_v1')) {
445 return;
446 }
447
448 if (!class_exists('Yatra\\Database\\Tables\\BookingsTable')) {
449 return;
450 }
451
452 $table = \Yatra\Database\Tables\BookingsTable::getTableName();
453 if (!self::databaseTableExists($table)) {
454 return;
455 }
456
457 global $wpdb;
458
459 $columnInfo = $wpdb->get_row(
460 $wpdb->prepare(
461 "SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
462 WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
463 DB_NAME,
464 $table,
465 'status'
466 )
467 );
468
469 $columnType = is_object($columnInfo) ? (string) ($columnInfo->COLUMN_TYPE ?? '') : '';
470 $needsAlter = $columnType !== '' && strpos($columnType, 'pending_verification') === false;
471
472 if ($needsAlter) {
473 // Match the original column shape exactly minus the new enum
474 // value — nullable, default 'pending', no NOT NULL.
475 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, enum literal is static.
476 $wpdb->query(
477 'ALTER TABLE `' . esc_sql($table) . "` "
478 . "MODIFY COLUMN `status` "
479 . "enum('pending','pending_verification','confirmed','processing','completed','cancelled','refunded','failed','on_hold','waitlist') "
480 . "DEFAULT 'pending'"
481 );
482 }
483
484 // Backfill: bookings whose insert hit the old ENUM during a
485 // verification flow ended up with status='' (silent coerce).
486 // Now that the enum accepts pending_verification, restore them.
487 // Filtered to a narrow signal (status='' AND payment_status='pending')
488 // so we don't accidentally re-stamp unrelated edge cases.
489 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper, literal values only.
490 $wpdb->query(
491 "UPDATE `{$table}` SET `status` = 'pending_verification' "
492 . "WHERE `status` = '' AND `payment_status` = 'pending'"
493 );
494
495 update_option('yatra_booking_status_pending_verification_v1', '1', false);
496 }
497
498 /**
499 * Fill canonical + legacy email identity options when empty (upgrades, partial installs, or empty strings in DB).
500 * Idempotent; safe to run on each admin load via maybeBackfillEmailTemplateDefaults().
501 */
502 public static function maybeBackfillEmailDeliveryIdentity(): void
503 {
504 $wpAdmin = trim((string) get_option('admin_email', ''));
505 $wpName = trim((string) get_bloginfo('name'));
506
507 $isUnsetOrEmpty = static function ($v): bool {
508 return $v === false || $v === null || $v === '' || (is_string($v) && trim($v) === '');
509 };
510
511 if ($wpAdmin !== '') {
512 foreach (['yatra_admin_email', 'yatra_from_email', 'yatra_email_from_address'] as $opt) {
513 if ($isUnsetOrEmpty(get_option($opt, false))) {
514 update_option($opt, $wpAdmin);
515 }
516 }
517 }
518 if ($wpName !== '') {
519 foreach (['yatra_from_name', 'yatra_email_from_name'] as $opt) {
520 if ($isUnsetOrEmpty(get_option($opt, false))) {
521 update_option($opt, $wpName);
522 }
523 }
524 }
525 }
526
527 /**
528 * One-time: persist default HTML subjects/bodies when options exist but are empty (pre-template-defaults installs).
529 */
530 public static function maybeBackfillEmailTemplateDefaults(): void
531 {
532 self::maybeBackfillEmailDeliveryIdentity();
533
534 // Default-on for new option on existing sites (add_option no-ops if already present).
535 add_option('yatra_email_template_admin_new_booking', 1);
536 add_option('yatra_email_template_admin_payment', 1);
537 add_option('yatra_email_template_admin_cancellation', 1);
538
539 self::maybeBackfillCustomerEmailVerificationTemplate();
540 self::maybeBackfillExtendedTransactionalEmailOptionsV2();
541
542 if (!get_option('yatra_email_identity_synced_v1')) {
543 $from = get_option('yatra_from_email', '');
544 if (($from === false || $from === '') && ($legacy = get_option('yatra_email_from_address', '')) && is_string($legacy) && $legacy !== '') {
545 update_option('yatra_from_email', $legacy);
546 }
547 $fname = get_option('yatra_from_name', '');
548 if (($fname === false || $fname === '') && ($legacy = get_option('yatra_email_from_name', '')) && is_string($legacy) && $legacy !== '') {
549 update_option('yatra_from_name', $legacy);
550 }
551 update_option('yatra_email_identity_synced_v1', '1');
552 }
553
554 if (get_option('yatra_email_tpl_defaults_backfill_1')) {
555 return;
556 }
557
558 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $key => $defaultValue) {
559 $name = 'yatra_' . $key;
560 $current = get_option($name, false);
561 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
562 if ($isEmpty) {
563 update_option($name, $defaultValue);
564 }
565 }
566
567 update_option('yatra_email_tpl_defaults_backfill_1', '1');
568 }
569
570 /**
571 * One-time: customer email verification template (Email → Templates) for existing installs.
572 */
573 private static function maybeBackfillCustomerEmailVerificationTemplate(): void
574 {
575 if (get_option('yatra_email_customer_verification_tpl_v1')) {
576 return;
577 }
578
579 add_option('yatra_email_template_customer_verification', true);
580 add_option('yatra_email_template_guest_verification', true);
581
582 $defaults = EmailTemplateDefaults::settingsOptionDefaults();
583 foreach (['email_tpl_customer_verification_subject', 'email_tpl_customer_verification_body'] as $key) {
584 if (!isset($defaults[$key])) {
585 continue;
586 }
587 $name = 'yatra_' . $key;
588 $current = get_option($name, false);
589 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
590 if ($isEmpty) {
591 update_option($name, $defaults[$key]);
592 }
593 }
594
595 update_option('yatra_email_customer_verification_tpl_v1', '1');
596 }
597
598 /**
599 * One-time: enable flags + default HTML for extended transactional templates (completed, expiry, scheduled, enquiry, review, abandoned).
600 * Only writes options that are still empty so existing customized HTML in the database is preserved on plugin update.
601 */
602 private static function maybeBackfillExtendedTransactionalEmailOptionsV2(): void
603 {
604 if (get_option('yatra_email_tpl_extended_v2')) {
605 return;
606 }
607
608 $boolFlags = [
609 'email_template_booking_completed',
610 'email_template_booking_expired_customer',
611 'email_template_admin_booking_expired',
612 'email_template_scheduled_payment_reminder',
613 'email_template_scheduled_payment_succeeded',
614 'email_template_scheduled_payment_failed',
615 'email_template_admin_scheduled_payment_failed',
616 'email_template_enquiry_received',
617 'email_template_enquiry_admin',
618 'email_template_enquiry_response',
619 'email_template_review_request',
620 'email_template_abandoned_booking_recovery_first',
621 'email_template_abandoned_booking_recovery_second',
622 'email_template_abandoned_booking_recovery_final',
623 ];
624 foreach ($boolFlags as $flag) {
625 add_option('yatra_' . $flag, true);
626 }
627
628 $extendedContentKeys = [
629 'email_tpl_booking_completed_subject',
630 'email_tpl_booking_completed_body',
631 'email_tpl_booking_expired_customer_subject',
632 'email_tpl_booking_expired_customer_body',
633 'email_tpl_admin_booking_expired_subject',
634 'email_tpl_admin_booking_expired_body',
635 'email_tpl_scheduled_payment_reminder_subject',
636 'email_tpl_scheduled_payment_reminder_body',
637 'email_tpl_scheduled_payment_succeeded_subject',
638 'email_tpl_scheduled_payment_succeeded_body',
639 'email_tpl_scheduled_payment_failed_subject',
640 'email_tpl_scheduled_payment_failed_body',
641 'email_tpl_admin_scheduled_payment_failed_subject',
642 'email_tpl_admin_scheduled_payment_failed_body',
643 'email_tpl_enquiry_admin_subject',
644 'email_tpl_enquiry_admin_body',
645 'email_tpl_enquiry_received_subject',
646 'email_tpl_enquiry_received_body',
647 'email_tpl_enquiry_response_subject',
648 'email_tpl_enquiry_response_body',
649 'email_tpl_review_request_subject',
650 'email_tpl_review_request_body',
651 'email_tpl_abandoned_booking_recovery_first_subject',
652 'email_tpl_abandoned_booking_recovery_first_body',
653 'email_tpl_abandoned_booking_recovery_second_subject',
654 'email_tpl_abandoned_booking_recovery_second_body',
655 'email_tpl_abandoned_booking_recovery_final_subject',
656 'email_tpl_abandoned_booking_recovery_final_body',
657 ];
658
659 $defaults = EmailTemplateDefaults::settingsOptionDefaults();
660 foreach ($extendedContentKeys as $key) {
661 if (!isset($defaults[$key])) {
662 continue;
663 }
664 $name = 'yatra_' . $key;
665 $current = get_option($name, false);
666 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
667 if ($isEmpty) {
668 update_option($name, $defaults[$key]);
669 }
670 }
671
672 update_option('yatra_email_tpl_extended_v2', '1');
673 }
674 }
675