PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
← All changes | app/Services/InstallerService.php +453 -80 3.0.2.7 → 3.0.16 View file →
@@ -35,13 +35,9 @@
35 35 {
36 36 if (class_exists('\Yatra\Core\Database')) {
37 37 \Yatra\Core\Database::createTables();
38 38 }
39 -
40 - // Log table creation (for debugging)
41 - if (defined('WP_DEBUG') && WP_DEBUG) {
42 - error_log('Yatra Installer: Created all database tables');
43 - }
39 +
44 40 }
45 41
46 42 /**
47 43 * Set all default options for fresh installation
@@ -51,15 +47,31 @@
51 47 * @return void
52 48 */
53 49 private static function setDefaultOptions(): void
54 50 {
51 + // Whether this is a brand-new install vs. a reactivation/upgrade.
52 + // Captured before version/date are stamped below so it reflects the
53 + // pre-activation state.
54 + $isFreshInstall = self::isFreshInstallation();
55 +
56 + // Seed a default only when the option is absent. add_option() is a
57 + // no-op when the option already exists (existence is keyed on the
58 + // option NAME, so values legitimately stored as false/0/'' are still
59 + // preserved). This makes activation idempotent: a deactivate ->
60 + // reactivate, or a plugin update, can never overwrite an operator's
61 + // saved settings, while a fresh install (and any newly-introduced
62 + // default on upgrade) is still seeded.
63 + $seed = static function (string $name, $value): void {
64 + add_option($name, $value);
65 + };
66 +
55 67 // Payment Gateway Settings - Only enable Pay Later by default
56 68 // These match SettingsService defaults exactly
57 - update_option('yatra_payment_gateways', ['pay_later']);
58 - update_option('yatra_payment_methods', []);
59 - update_option('yatra_payment_test_mode', true);
60 - update_option('yatra_auto_confirm_pay_later', true);
61 - update_option('yatra_partial_payment', false);
69 + $seed('yatra_payment_gateways', ['pay_later']);
70 + $seed('yatra_payment_methods', []);
71 + $seed('yatra_payment_test_mode', true);
72 + $seed('yatra_auto_confirm_pay_later', true);
73 + $seed('yatra_partial_payment', false);
62 74 // Set gateway configs with proper structure - only enable pay_later by default
63 75 $gateway_configs = [
64 76 'pay_later' => [
65 77 'enabled' => true,
@@ -110,97 +122,111 @@
110 122 'api_key' => '',
111 123 'api_secret' => '',
112 124 ]
113 125 ];
114 - update_option('yatra_gateway_configs', $gateway_configs);
115 - update_option('yatra_gateway_order', []);
126 + $seed('yatra_gateway_configs', $gateway_configs);
127 + $seed('yatra_gateway_order', []);
116 128
117 129 // Currency Settings - Match SettingsService defaults
118 - update_option('yatra_currency', 'USD');
119 - update_option('yatra_currency_position', 'before');
120 - update_option('yatra_thousand_separator', ',');
121 - update_option('yatra_decimal_separator', '.');
122 - update_option('yatra_decimal_places', 2);
130 + $seed('yatra_currency', 'USD');
131 + $seed('yatra_currency_position', 'before');
132 + $seed('yatra_thousand_separator', ',');
133 + $seed('yatra_decimal_separator', '.');
134 + $seed('yatra_decimal_places', 2);
123 135
124 136 // Flexible Payment Settings - Match SettingsService defaults
125 - update_option('yatra_enable_deposit', false);
126 - update_option('yatra_deposit_type', 'percentage');
127 - update_option('yatra_deposit_amount', 20);
128 - update_option('yatra_deposit_required', false);
129 - update_option('yatra_deposit_percentage', 20);
130 - update_option('yatra_partial_payment_percentage', 30);
137 + $seed('yatra_enable_deposit', false);
138 + $seed('yatra_deposit_type', 'percentage');
139 + $seed('yatra_deposit_amount', 20);
140 + $seed('yatra_deposit_required', false);
141 + $seed('yatra_deposit_percentage', 20);
142 + $seed('yatra_partial_payment_percentage', 30);
131 143
132 - // Scheduled Payment Settings - Match SettingsService defaults
133 - update_option('yatra_enable_scheduled_payments', false);
134 - update_option('yatra_scheduled_payment_type', 'single');
135 - update_option('yatra_scheduled_payment_days', 15);
136 - update_option('yatra_scheduled_payment_installments', 1);
137 - update_option('yatra_scheduled_payment_interval', 30);
138 - update_option('yatra_scheduled_payment_reminder_days', 3);
139 - update_option('yatra_allow_save_payment_methods', false);
144 + $seed('yatra_allow_save_payment_methods', false);
140 145
141 146 // Trip Settings - Match SettingsService defaults
142 - update_option('yatra_trip_base', 'trip');
143 - update_option('yatra_trips_per_page', 12);
144 - update_option('yatra_enable_wishlist', false);
145 - update_option('yatra_enable_comparison', false);
146 - update_option('yatra_show_sold_out', true);
147 + $seed('yatra_trip_base', 'trip');
148 + $seed('yatra_trips_per_page', 12);
149 + $seed('yatra_enable_wishlist', false);
150 + $seed('yatra_enable_comparison', false);
151 + $seed('yatra_show_sold_out', true);
147 152
148 153 // Customer Settings - Match SettingsService defaults
149 - update_option('yatra_enable_customer_accounts', true);
150 - update_option('yatra_enable_customer_registration', true);
154 + $seed('yatra_enable_customer_accounts', true);
155 + $seed('yatra_enable_customer_registration', true);
151 156
152 157 // Booking Settings - Match SettingsService defaults
153 - update_option('yatra_booking_base', 'book');
154 - update_option('yatra_use_booking_page', false);
155 - update_option('yatra_booking_page_id', 0);
156 - update_option('yatra_enable_guest_booking', true);
157 - update_option('yatra_booking_confirmation', true);
158 - update_option('yatra_auto_confirm_bookings', false);
159 - update_option('yatra_require_login', false);
160 - update_option('yatra_allow_guest_checkout', true);
161 - update_option('yatra_cancellation_policy', 'full_refund');
162 - update_option('yatra_cancellation_days', 7);
163 - update_option('yatra_refund_policy', '');
164 - update_option('yatra_booking_expiry_hours', 24);
165 - update_option('yatra_booking_reminder_days', 3);
166 - update_option('yatra_allow_waitlist', true);
158 + $seed('yatra_booking_base', 'book');
159 + $seed('yatra_use_booking_page', false);
160 + $seed('yatra_booking_page_id', 0);
161 + $seed('yatra_enable_guest_booking', true);
162 + $seed('yatra_booking_confirmation', true);
163 + $seed('yatra_auto_confirm_bookings', false);
164 + $seed('yatra_require_login', false);
165 + $seed('yatra_allow_guest_checkout', true);
166 + // cancellation_policy / cancellation_days / refund_policy
167 + // intentionally not seeded — these are removed settings (see
168 + // SettingsController::$default_settings comment). Existing
169 + // sites that already have orphan values stored will keep
170 + // them in wp_options; new sites won't acquire them.
171 + $seed('yatra_booking_expiry_hours', 24);
172 + $seed('yatra_booking_reminder_days', 3);
173 + $seed('yatra_allow_waitlist', true);
167 174
168 175 // Email identity: canonical keys (REST / EmailService) + legacy keys for older code paths
169 176 $wpAdminEmail = (string) get_option('admin_email', '');
170 177 $blogName = (string) get_bloginfo('name');
171 - update_option('yatra_from_email', $wpAdminEmail);
172 - update_option('yatra_from_name', $blogName);
173 - update_option('yatra_admin_email', $wpAdminEmail);
174 - update_option('yatra_email_from_name', $blogName);
175 - update_option('yatra_email_from_address', $wpAdminEmail);
176 - update_option('yatra_enable_admin_notifications', true);
177 - update_option('yatra_enable_customer_notifications', true);
178 + $seed('yatra_from_email', $wpAdminEmail);
179 + $seed('yatra_from_name', $blogName);
180 + $seed('yatra_admin_email', $wpAdminEmail);
181 + $seed('yatra_email_from_name', $blogName);
182 + $seed('yatra_email_from_address', $wpAdminEmail);
183 + $seed('yatra_enable_admin_notifications', true);
184 + $seed('yatra_enable_customer_notifications', true);
178 185
179 186 // Default transactional template HTML + subjects (Email → Templates / settings API)
180 187 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $optionKey => $value) {
181 - update_option('yatra_' . $optionKey, $value);
188 + $seed('yatra_' . $optionKey, $value);
182 189 }
183 - update_option('yatra_email_template_booking', true);
184 - update_option('yatra_email_template_confirmation', true);
185 - update_option('yatra_email_template_cancellation', true);
186 - update_option('yatra_email_template_reminder', true);
187 - update_option('yatra_email_template_admin_new_booking', true);
188 - update_option('yatra_email_template_admin_payment', true);
189 - update_option('yatra_email_template_admin_cancellation', true);
190 + $seed('yatra_email_template_booking', true);
191 + $seed('yatra_email_template_confirmation', true);
192 + $seed('yatra_email_template_cancellation', true);
193 + $seed('yatra_email_template_reminder', true);
194 + $seed('yatra_email_template_admin_new_booking', true);
195 + $seed('yatra_email_template_admin_payment', true);
196 + $seed('yatra_email_template_admin_cancellation', true);
197 + $seed('yatra_email_template_trip_consent', true);
198 + $seed('yatra_email_template_customer_verification', true);
199 + $seed('yatra_email_template_guest_verification', true);
200 + $seed('yatra_email_template_booking_completed', true);
201 + $seed('yatra_email_template_booking_expired_customer', true);
202 + $seed('yatra_email_template_admin_booking_expired', true);
203 + $seed('yatra_email_template_scheduled_payment_reminder', true);
204 + $seed('yatra_email_template_scheduled_payment_succeeded', true);
205 + $seed('yatra_email_template_scheduled_payment_failed', true);
206 + $seed('yatra_email_template_admin_scheduled_payment_failed', true);
207 + $seed('yatra_email_template_enquiry_received', true);
208 + $seed('yatra_email_template_enquiry_admin', true);
209 + $seed('yatra_email_template_enquiry_response', true);
210 + $seed('yatra_email_template_review_request', true);
211 + $seed('yatra_email_template_abandoned_booking_recovery_first', true);
212 + $seed('yatra_email_template_abandoned_booking_recovery_second', true);
213 + $seed('yatra_email_template_abandoned_booking_recovery_final', true);
214 +
215 + // Clear pre-existing legacy Stripe/PayPal settings, but only on a
216 + // brand-new install. On a reactivation these may hold the operator's
217 + // configured gateway data, so deleting them would be destructive.
218 + if ($isFreshInstall) {
219 + delete_option('yatra_stripe_settings');
220 + delete_option('yatra_paypal_settings');
221 + }
190 222
191 - // Clear any existing Stripe/PayPal settings that might exist
192 - delete_option('yatra_stripe_settings');
193 - delete_option('yatra_paypal_settings');
223 + // Installation tracking: stamp the date once (seed); keep the
224 + // version current so upgrade routines can detect version changes.
225 + $seed('yatra_installation_date', current_time('mysql'));
226 + update_option('yatra_version', defined('YATRA_VERSION') ? YATRA_VERSION : '3.0.3');
194 227
195 - // Set installation tracking (not in SettingsService but needed for tracking)
196 - update_option('yatra_installation_date', current_time('mysql'));
197 - update_option('yatra_version', defined('YATRA_VERSION') ? YATRA_VERSION : '3.0.2.4');
198 -
199 - // Log the installation (for debugging)
200 - if (defined('WP_DEBUG') && WP_DEBUG) {
201 - error_log('Yatra Installer: Set all default options with pay_later as only gateway');
202 - }
228 +
203 229 }
204 230
205 231 /**
206 232 * Get all required database tables using Table classes
@@ -213,10 +239,8 @@
213 239 $table_classes = [
214 240 \Yatra\Database\Tables\TripsTable::class,
215 241 \Yatra\Database\Tables\BookingsTable::class,
216 242 \Yatra\Database\Tables\BookingPaymentsTable::class,
217 - \Yatra\Database\Tables\ScheduledPaymentsTable::class,
218 - \Yatra\Database\Tables\PaymentTokensTable::class,
219 243 \Yatra\Database\Tables\CustomersTable::class,
220 244 \Yatra\Database\Tables\BookingTravellersTable::class,
221 245 \Yatra\Database\Tables\BookingTravellerMetaTable::class,
222 246 \Yatra\Database\Tables\BookingDeparturesTable::class,
@@ -317,8 +341,249 @@
317 341 update_option('yatra_discount_active_status_normalized_v1', '1', false);
318 342 }
319 343
320 344 /**
345 + * One-time normalization of recurring availability rules created against the
346 + * legacy schema (only `recurrence_type`, `capacity_value`, `interval`, etc.
347 + * were written) so the new admin React UI — which reads `rule_type`,
348 + * `seats_total`, `interval_days`, `interval_start_date` — can render and
349 + * edit them without showing phantom "1 on All / Active" badges or empty
350 + * "Every " patterns.
351 + *
352 + * What this fixes:
353 + * - Sample-data and pre-3.x rows landed with `rule_type` defaulted to
354 + * 'weekly' regardless of the actual `recurrence_type`, and with
355 + * `seats_total` left NULL (the new capacity column). Daily and monthly
356 + * rules therefore appeared as broken weekly rows in the new UI.
357 + * - The /counts endpoint correctly reported 1 active rule, but the list
358 + * table couldn't render it cleanly, leading users to read the API
359 + * response as "ghost data".
360 + *
361 + * Invariants:
362 + * - Idempotent — every UPDATE filters rows whose new columns are still
363 + * unset, so re-running is a no-op once the data is healed.
364 + * - Read-only on rows already authored by the new UI (`rule_type` already
365 + * matches the recurrence intent), so user edits are never overwritten.
366 + * - No-ops cleanly when the rules table doesn't exist yet (fresh install
367 + * before {@see \Yatra\Core\Database::createTables()} has run).
368 + */
369 + public static function maybeNormalizeAvailabilityRulesLegacyData(): void
370 + {
371 + if (get_option('yatra_availability_rules_legacy_normalized_v1')) {
372 + return;
373 + }
374 +
375 + if (!class_exists('Yatra\\Database\\Tables\\TripAvailabilityRulesTable')) {
376 + return;
377 + }
378 +
379 + $table = \Yatra\Database\Tables\TripAvailabilityRulesTable::getTableName();
380 + if (!self::databaseTableExists($table)) {
381 + return;
382 + }
383 +
384 + global $wpdb;
385 +
386 + // 1. Daily-recurrence rows whose `rule_type` defaulted to 'weekly':
387 + // map to the new "interval" rule type and copy the legacy `interval`
388 + // + `start_date` into the new columns the React form binds to.
389 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
390 + $wpdb->query("UPDATE `{$table}`
391 + SET `rule_type` = 'interval',
392 + `interval_days` = COALESCE(`interval_days`, NULLIF(`interval`, 0), 1),
393 + `interval_start_date` = COALESCE(`interval_start_date`, `start_date`)
394 + WHERE `recurrence_type` = 'daily'
395 + AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
396 +
397 + // 2. Monthly-recurrence rows whose `rule_type` defaulted to 'weekly':
398 + // relabel to 'monthly'. The new UI uses (week_of_month, day_of_week)
399 + // rather than `day_of_month`, so we leave those NULL for the user
400 + // to set in the form rather than guess from the legacy day_of_month.
401 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
402 + $wpdb->query("UPDATE `{$table}`
403 + SET `rule_type` = 'monthly'
404 + WHERE `recurrence_type` = 'monthly'
405 + AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
406 +
407 + // 3. Weekly-recurrence rows: ensure `rule_type` is set explicitly
408 + // (most already match the default; this catches any NULL/empty).
409 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
410 + $wpdb->query("UPDATE `{$table}`
411 + SET `rule_type` = 'weekly'
412 + WHERE `recurrence_type` = 'weekly'
413 + AND (`rule_type` IS NULL OR `rule_type` = '')");
414 +
415 + // 4. seats_total backfill from `capacity_value` for fixed-capacity rows
416 + // so CapacityService and the React table both surface the right
417 + // seat cap without falling through hydrate-time fallbacks.
418 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
419 + $wpdb->query("UPDATE `{$table}`
420 + SET `seats_total` = `capacity_value`
421 + WHERE `seats_total` IS NULL
422 + AND `capacity_value` IS NOT NULL
423 + AND `capacity_value` > 0
424 + AND (`capacity_type` IS NULL OR `capacity_type` = 'fixed')");
425 +
426 + update_option('yatra_availability_rules_legacy_normalized_v1', '1', false);
427 + }
428 +
429 + /**
430 + * Ensure `wp_yatra_bookings.status` accepts `pending_verification`.
431 + *
432 + * The 3.0.5 guest email-verification feature introduced a new holding
433 + * status (`pending_verification`) but the production ENUM only listed
434 + * the legacy values. Until the column is widened, MySQL non-strict
435 + * mode silently coerces the value to `''` on insert — which makes
436 + * every booking placed with verification enabled appear broken:
437 + *
438 + * 1. The admin booking list renders an empty status badge (the
439 + * React status map has no entry for `''` so the row falls into
440 + * the default branch with an empty label).
441 + * 2. `BookingService::createBooking` sees the in-memory
442 + * `$data['status'] === 'pending_verification'` and defers
443 + * firing `yatra_booking_created`, so the customer never gets
444 + * the booking-confirmation email.
445 + * 3. `verify_email` reads the persisted status (now `''`),
446 + * decides the booking is "already verified", skips the deferred
447 + * fan-out — so neither the status flip nor the booking email
448 + * ever fires.
449 + *
450 + * Doing the widening here (runIdempotentMaintenance — every admin
451 + * pageview) instead of a pure version-gated upgrade step means it
452 + * heals installs whose stored yatra_version was already bumped to
453 + * 3.0.5 by an earlier failed upgrade attempt. Cheap: one
454 + * INFORMATION_SCHEMA query gated by a one-shot option flag, ALTER
455 + * runs at most once per install.
456 + *
457 + * Also backfills any rows whose status was silently coerced to `''`
458 + * by the pre-widening insert path: those bookings *should* have
459 + * landed in `pending_verification`, so we restore them there. The
460 + * original verify-email magic link still works because the HMAC
461 + * token is bound to booking_id + email, not status.
462 + */
463 + public static function maybeAddPendingVerificationBookingStatus(): void
464 + {
465 + if (get_option('yatra_booking_status_pending_verification_v1')) {
466 + return;
467 + }
468 +
469 + if (!class_exists('Yatra\\Database\\Tables\\BookingsTable')) {
470 + return;
471 + }
472 +
473 + $table = \Yatra\Database\Tables\BookingsTable::getTableName();
474 + if (!self::databaseTableExists($table)) {
475 + return;
476 + }
477 +
478 + global $wpdb;
479 +
480 + $columnInfo = $wpdb->get_row(
481 + $wpdb->prepare(
482 + "SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
483 + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
484 + DB_NAME,
485 + $table,
486 + 'status'
487 + )
488 + );
489 +
490 + $columnType = is_object($columnInfo) ? (string) ($columnInfo->COLUMN_TYPE ?? '') : '';
491 + $needsAlter = $columnType !== '' && strpos($columnType, 'pending_verification') === false;
492 +
493 + if ($needsAlter) {
494 + // Match the original column shape exactly minus the new enum
495 + // value — nullable, default 'pending', no NOT NULL.
496 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, enum literal is static.
497 + $wpdb->query(
498 + 'ALTER TABLE `' . esc_sql($table) . "` "
499 + . "MODIFY COLUMN `status` "
500 + . "enum('pending','pending_verification','confirmed','processing','completed','cancelled','refunded','failed','on_hold','waitlist') "
501 + . "DEFAULT 'pending'"
502 + );
503 + }
504 +
505 + // Backfill: bookings whose insert hit the old ENUM during a
506 + // verification flow ended up with status='' (silent coerce).
507 + // Now that the enum accepts pending_verification, restore them.
508 + // Filtered to a narrow signal (status='' AND payment_status='pending')
509 + // so we don't accidentally re-stamp unrelated edge cases.
510 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper, literal values only.
511 + $wpdb->query(
512 + "UPDATE `{$table}` SET `status` = 'pending_verification' "
513 + . "WHERE `status` = '' AND `payment_status` = 'pending'"
514 + );
515 +
516 + update_option('yatra_booking_status_pending_verification_v1', '1', false);
517 + }
518 +
519 + /**
520 + * Add the `duration_hours` column to the trips table for hour-based
521 + * (single-day) tours. Purely additive and nullable — existing trips get
522 + * NULL and behave exactly as before (day-based via `duration_days`). Only
523 + * tours that later set a positive `duration_hours` change behaviour.
524 + *
525 + * Idempotent: guarded by a one-shot option AND an INFORMATION_SCHEMA check,
526 + * so it runs its ALTER at most once and is a no-op when the column already
527 + * exists (fresh installs get it from TripsTable::getSchema()).
528 + */
529 + public static function maybeAddTripDurationHoursColumn(): void
530 + {
531 + if (get_option('yatra_trip_duration_hours_v1')) {
532 + return;
533 + }
534 +
535 + if (!class_exists('Yatra\\Database\\Tables\\TripsTable')) {
536 + return;
537 + }
538 +
539 + $table = \Yatra\Database\Tables\TripsTable::getTableName();
540 + if (!self::databaseTableExists($table)) {
541 + return;
542 + }
543 +
544 + global $wpdb;
545 +
546 + $columnExists = $wpdb->get_var(
547 + $wpdb->prepare(
548 + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
549 + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
550 + DB_NAME,
551 + $table,
552 + 'duration_hours'
553 + )
554 + );
555 +
556 + if (!$columnExists) {
557 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, static column definition.
558 + $wpdb->query(
559 + 'ALTER TABLE `' . esc_sql($table) . '` '
560 + . "ADD COLUMN `duration_hours` smallint(5) UNSIGNED DEFAULT NULL "
561 + . "COMMENT 'Duration in hours for hour-based single-day tours, NULL means day-based' "
562 + . 'AFTER `duration_nights`'
563 + );
564 +
565 + // Re-check so we only mark this done when the column really exists.
566 + // If the ALTER failed (e.g. a restrictive host), leave the one-shot
567 + // flag unset so it retries on the next admin load rather than
568 + // disabling the feature permanently.
569 + $columnExists = $wpdb->get_var(
570 + $wpdb->prepare(
571 + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
572 + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
573 + DB_NAME,
574 + $table,
575 + 'duration_hours'
576 + )
577 + );
578 + }
579 +
580 + if ($columnExists) {
581 + update_option('yatra_trip_duration_hours_v1', '1', false);
582 + }
583 + }
584 +
585 + /**
321 586 * Fill canonical + legacy email identity options when empty (upgrades, partial installs, or empty strings in DB).
322 587 * Idempotent; safe to run on each admin load via maybeBackfillEmailTemplateDefaults().
323 588 */
324 589 public static function maybeBackfillEmailDeliveryIdentity(): void
@@ -357,8 +622,11 @@
357 622 add_option('yatra_email_template_admin_new_booking', 1);
358 623 add_option('yatra_email_template_admin_payment', 1);
359 624 add_option('yatra_email_template_admin_cancellation', 1);
360 625
626 + self::maybeBackfillCustomerEmailVerificationTemplate();
627 + self::maybeBackfillExtendedTransactionalEmailOptionsV2();
628 +
361 629 if (!get_option('yatra_email_identity_synced_v1')) {
362 630 $from = get_option('yatra_from_email', '');
363 631 if (($from === false || $from === '') && ($legacy = get_option('yatra_email_from_address', '')) && is_string($legacy) && $legacy !== '') {
364 632 update_option('yatra_from_email', $legacy);
@@ -383,6 +651,111 @@
383 651 }
384 652 }
385 653
386 654 update_option('yatra_email_tpl_defaults_backfill_1', '1');
655 + }
656 +
657 + /**
658 + * One-time: customer email verification template (Email → Templates) for existing installs.
659 + */
660 + private static function maybeBackfillCustomerEmailVerificationTemplate(): void
661 + {
662 + if (get_option('yatra_email_customer_verification_tpl_v1')) {
663 + return;
664 + }
665 +
666 + add_option('yatra_email_template_customer_verification', true);
667 + add_option('yatra_email_template_guest_verification', true);
668 +
669 + $defaults = EmailTemplateDefaults::settingsOptionDefaults();
670 + foreach (['email_tpl_customer_verification_subject', 'email_tpl_customer_verification_body'] as $key) {
671 + if (!isset($defaults[$key])) {
672 + continue;
673 + }
674 + $name = 'yatra_' . $key;
675 + $current = get_option($name, false);
676 + $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
677 + if ($isEmpty) {
678 + update_option($name, $defaults[$key]);
679 + }
680 + }
681 +
682 + update_option('yatra_email_customer_verification_tpl_v1', '1');
683 + }
684 +
685 + /**
686 + * One-time: enable flags + default HTML for extended transactional templates (completed, expiry, scheduled, enquiry, review, abandoned).
687 + * Only writes options that are still empty so existing customized HTML in the database is preserved on plugin update.
688 + */
689 + private static function maybeBackfillExtendedTransactionalEmailOptionsV2(): void
690 + {
691 + if (get_option('yatra_email_tpl_extended_v2')) {
692 + return;
693 + }
694 +
695 + $boolFlags = [
696 + 'email_template_booking_completed',
697 + 'email_template_booking_expired_customer',
698 + 'email_template_admin_booking_expired',
699 + 'email_template_scheduled_payment_reminder',
700 + 'email_template_scheduled_payment_succeeded',
701 + 'email_template_scheduled_payment_failed',
702 + 'email_template_admin_scheduled_payment_failed',
703 + 'email_template_enquiry_received',
704 + 'email_template_enquiry_admin',
705 + 'email_template_enquiry_response',
706 + 'email_template_review_request',
707 + 'email_template_abandoned_booking_recovery_first',
708 + 'email_template_abandoned_booking_recovery_second',
709 + 'email_template_abandoned_booking_recovery_final',
710 + ];
711 + foreach ($boolFlags as $flag) {
712 + add_option('yatra_' . $flag, true);
713 + }
714 +
715 + $extendedContentKeys = [
716 + 'email_tpl_booking_completed_subject',
717 + 'email_tpl_booking_completed_body',
718 + 'email_tpl_booking_expired_customer_subject',
719 + 'email_tpl_booking_expired_customer_body',
720 + 'email_tpl_admin_booking_expired_subject',
721 + 'email_tpl_admin_booking_expired_body',
722 + 'email_tpl_scheduled_payment_reminder_subject',
723 + 'email_tpl_scheduled_payment_reminder_body',
724 + 'email_tpl_scheduled_payment_succeeded_subject',
725 + 'email_tpl_scheduled_payment_succeeded_body',
726 + 'email_tpl_scheduled_payment_failed_subject',
727 + 'email_tpl_scheduled_payment_failed_body',
728 + 'email_tpl_admin_scheduled_payment_failed_subject',
729 + 'email_tpl_admin_scheduled_payment_failed_body',
730 + 'email_tpl_enquiry_admin_subject',
731 + 'email_tpl_enquiry_admin_body',
732 + 'email_tpl_enquiry_received_subject',
733 + 'email_tpl_enquiry_received_body',
734 + 'email_tpl_enquiry_response_subject',
735 + 'email_tpl_enquiry_response_body',
736 + 'email_tpl_review_request_subject',
737 + 'email_tpl_review_request_body',
738 + 'email_tpl_abandoned_booking_recovery_first_subject',
739 + 'email_tpl_abandoned_booking_recovery_first_body',
740 + 'email_tpl_abandoned_booking_recovery_second_subject',
741 + 'email_tpl_abandoned_booking_recovery_second_body',
742 + 'email_tpl_abandoned_booking_recovery_final_subject',
743 + 'email_tpl_abandoned_booking_recovery_final_body',
744 + ];
745 +
746 + $defaults = EmailTemplateDefaults::settingsOptionDefaults();
747 + foreach ($extendedContentKeys as $key) {
748 + if (!isset($defaults[$key])) {
749 + continue;
750 + }
751 + $name = 'yatra_' . $key;
752 + $current = get_option($name, false);
753 + $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
754 + if ($isEmpty) {
755 + update_option($name, $defaults[$key]);
756 + }
757 + }
758 +
759 + update_option('yatra_email_tpl_extended_v2', '1');
387 760 }
388 761 }