// reactivate, or a plugin update, can never overwrite an operator's // saved settings, while a fresh install (and any newly-introduced // default on upgrade) is still seeded. $seed = static function (string $name, $value): void { add_option($name, $value); }; // Payment Gateway Settings - Only enable Pay Later by default // These match SettingsService defaults exactly $seed('yatra_payment_gateways', ['pay_later']); $seed('yatra_payment_methods', []); $seed('yatra_payment_test_mode', true); $seed('yatra_auto_confirm_pay_later', true); $seed('yatra_partial_payment', false); // Set gateway configs with proper structure - only enable pay_later by default $gateway_configs = [ 'pay_later' => [ 'enabled' => true, 'title' => 'Book Now, Pay Later', 'description' => 'Allow customers to reserve now and pay before the trip', ], // Explicitly disable all other gateways 'stripe' => [ 'enabled' => false, 'title' => 'Stripe', 'description' => 'Accept credit and debit cards', 'api_key' => '', 'api_secret' => '', 'webhook_secret' => '', ], 'paypal' => [ 'enabled' => false, 'title' => 'PayPal', 'description' => 'Accept PayPal payments', 'api_key' => '', 'api_secret' => '', ], 'razorpay' => [ 'enabled' => false, 'title' => 'Razorpay', 'description' => 'Accept payments via Razorpay', 'api_key' => '', 'api_secret' => '', ], 'square' => [ 'enabled' => false, 'title' => 'Square', 'description' => 'Accept payments via Square', 'api_key' => '', 'api_secret' => '', ], 'authorize_net' => [ 'enabled' => false, 'title' => 'Authorize.net', 'description' => 'Accept payments via Authorize.net', 'api_key' => '', 'api_secret' => '', ], 'bank_transfer' => [ 'enabled' => false, 'title' => 'Bank Transfer', 'description' => 'Accept manual bank transfer payments', 'api_key' => '', 'api_secret' => '', ] ]; $seed('yatra_gateway_configs', $gateway_configs); $seed('yatra_gateway_order', []); // Currency Settings - Match SettingsService defaults $seed('yatra_currency', 'USD'); $seed('yatra_currency_position', 'before'); $seed('yatra_thousand_separator', ','); $seed('yatra_decimal_separator', '.'); $seed('yatra_decimal_places', 2); // Flexible Payment Settings - Match SettingsService defaults $seed('yatra_enable_deposit', false); $seed('yatra_deposit_type', 'percentage'); $seed('yatra_deposit_amount', 20); $seed('yatra_deposit_required', false); $seed('yatra_deposit_percentage', 20); $seed('yatra_partial_payment_percentage', 30); $seed('yatra_allow_save_payment_methods', false); // Trip Settings - Match SettingsService defaults $seed('yatra_trip_base', 'trip'); $seed('yatra_trips_per_page', 12); $seed('yatra_enable_wishlist', false); $seed('yatra_enable_comparison', false); $seed('yatra_show_sold_out', true); // Customer Settings - Match SettingsService defaults $seed('yatra_enable_customer_accounts', true); $seed('yatra_enable_customer_registration', true); // Booking Settings - Match SettingsService defaults $seed('yatra_booking_base', 'book'); $seed('yatra_use_booking_page', false); $seed('yatra_booking_page_id', 0); $seed('yatra_enable_guest_booking', true); $seed('yatra_booking_confirmation', true); $seed('yatra_auto_confirm_bookings', false); $seed('yatra_require_login', false); $seed('yatra_allow_guest_checkout', true); // cancellation_policy / cancellation_days / refund_policy // intentionally not seeded — these are removed settings (see // SettingsController::$default_settings comment). Existing // sites that already have orphan values stored will keep // them in wp_options; new sites won't acquire them. $seed('yatra_booking_expiry_hours', 24); $seed('yatra_booking_reminder_days', 3); $seed('yatra_allow_waitlist', true); // Email identity: canonical keys (REST / EmailService) + legacy keys for older code paths $wpAdminEmail = (string) get_option('admin_email', ''); $blogName = (string) get_bloginfo('name'); $seed('yatra_from_email', $wpAdminEmail); $seed('yatra_from_name', $blogName); $seed('yatra_admin_email', $wpAdminEmail); $seed('yatra_email_from_name', $blogName); $seed('yatra_email_from_address', $wpAdminEmail); $seed('yatra_enable_admin_notifications', true); $seed('yatra_enable_customer_notifications', true); // Default transactional template HTML + subjects (Email → Templates / settings API) foreach (EmailTemplateDefaults::settingsOptionDefaults() as $optionKey => $value) { $seed('yatra_' . $optionKey, $value); } $seed('yatra_email_template_booking', true); $seed('yatra_email_template_confirmation', true); $seed('yatra_email_template_cancellation', true); $seed('yatra_email_template_reminder', true); $seed('yatra_email_template_admin_new_booking', true); $seed('yatra_email_template_admin_payment', true); $seed('yatra_email_template_admin_cancellation', true); $seed('yatra_email_template_trip_consent', true); $seed('yatra_email_template_customer_verification', true); $seed('yatra_email_template_guest_verification', true); $seed('yatra_email_template_booking_completed', true); $seed('yatra_email_template_booking_expired_customer', true); $seed('yatra_email_template_admin_booking_expired', true); $seed('yatra_email_template_scheduled_payment_reminder', true); $seed('yatra_email_template_scheduled_payment_succeeded', true); $seed('yatra_email_template_scheduled_payment_failed', true); $seed('yatra_email_template_admin_scheduled_payment_failed', true); $seed('yatra_email_template_enquiry_received', true); $seed('yatra_email_template_enquiry_admin', true); $seed('yatra_email_template_enquiry_response', true); $seed('yatra_email_template_review_request', true); $seed('yatra_email_template_abandoned_booking_recovery_first', true); $seed('yatra_email_template_abandoned_booking_recovery_second', true); $seed('yatra_email_template_abandoned_booking_recovery_final', true); // Clear pre-existing legacy Stripe/PayPal settings, but only on a // brand-new install. On a reactivation these may hold the operator's // configured gateway data, so deleting them would be destructive. if ($isFreshInstall) { delete_option('yatra_stripe_settings'); delete_option('yatra_paypal_settings'); } // Installation tracking: stamp the date once (seed); keep the // version current so upgrade routines can detect version changes. $seed('yatra_installation_date', current_time('mysql')); update_option('yatra_version', defined('YATRA_VERSION') ? YATRA_VERSION : '3.0.3'); } /** * Get all required database tables using Table classes * * @return array */ public static function getRequiredTables(): array { // Must match \Yatra\Core\Database::createTables() — used for activation, migrations, and targeted checks. $table_classes = [ \Yatra\Database\Tables\TripsTable::class, \Yatra\Database\Tables\BookingsTable::class, \Yatra\Database\Tables\BookingPaymentsTable::class, \Yatra\Database\Tables\CustomersTable::class, \Yatra\Database\Tables\BookingTravellersTable::class, \Yatra\Database\Tables\BookingTravellerMetaTable::class, \Yatra\Database\Tables\BookingDeparturesTable::class, \Yatra\Database\Tables\ReviewsTable::class, \Yatra\Database\Tables\DiscountsTable::class, \Yatra\Database\Tables\EnquiriesTable::class, \Yatra\Database\Tables\TripAvailabilityDatesTable::class, \Yatra\Database\Tables\TripAvailabilityRulesTable::class, \Yatra\Database\Tables\TripRevisionsTable::class, \Yatra\Database\Tables\DeparturesTable::class, \Yatra\Database\Tables\TripItineraryDaysTable::class, \Yatra\Database\Tables\TripItineraryDayEntryTable::class, \Yatra\Database\Tables\ClassificationsTable::class, \Yatra\Database\Tables\TripClassificationsTable::class, \Yatra\Database\Tables\TripContentTable::class, ]; $table_names = []; foreach ($table_classes as $table_class) { if (class_exists($table_class)) { $table_names[] = $table_class::getTableName(); } } return $table_names; } /** * Whether a prefixed table exists. Uses esc_like() because SQL LIKE treats "_" as a wildcard. */ public static function databaseTableExists(string $fullTableName): bool { global $wpdb; if ($fullTableName === '') { return false; } $pattern = $wpdb->esc_like($fullTableName); $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $pattern)); return $found === $fullTableName; } /** * Check if this is a fresh installation * * @return bool */ public static function isFreshInstallation(): bool { // Check if Yatra version exists in database $installed_version = get_option('yatra_version'); // If no version is set, it's a fresh installation if ($installed_version === false) { return true; } // Check installation date $installation_date = get_option('yatra_installation_date'); if ($installation_date === false) { return true; } // Additional check: if core tables don't exist, it's fresh $required_tables = self::getRequiredTables(); if (!empty($required_tables)) { $trips_table = $required_tables[0]; // Use first table (already has prefix) if (!self::databaseTableExists($trips_table)) { return true; } } return false; } /** * One-time: coupon migration incorrectly stored status "active"; 3.x uses "publish" (admin + checkout). */ public static function maybeNormalizeMigratedCouponDiscountStatuses(): void { if (get_option('yatra_discount_active_status_normalized_v1')) { return; } if (!class_exists('Yatra\\Database\\Tables\\DiscountsTable')) { return; } $table = \Yatra\Database\Tables\DiscountsTable::getTableName(); if (!self::databaseTableExists($table)) { return; } global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper $wpdb->query("UPDATE `{$table}` SET `status` = 'publish' WHERE `status` = 'active'"); update_option('yatra_discount_active_status_normalized_v1', '1', false); } /** * One-time normalization of recurring availability rules created against the * legacy schema (only `recurrence_type`, `capacity_value`, `interval`, etc. * were written) so the new admin React UI — which reads `rule_type`, * `seats_total`, `interval_days`, `interval_start_date` — can render and * edit them without showing phantom "1 on All / Active" badges or empty * "Every " patterns. * * What this fixes: * - Sample-data and pre-3.x rows landed with `rule_type` defaulted to * 'weekly' regardless of the actual `recurrence_type`, and with * `seats_total` left NULL (the new capacity column). Daily and monthly * rules therefore appeared as broken weekly rows in the new UI. * - The /counts endpoint correctly reported 1 active rule, but the list * table couldn't render it cleanly, leading users to read the API * response as "ghost data". * * Invariants: * - Idempotent — every UPDATE filters rows whose new columns are still * unset, so re-running is a no-op once the data is healed. * - Read-only on rows already authored by the new UI (`rule_type` already * matches the recurrence intent), so user edits are never overwritten. * - No-ops cleanly when the rules table doesn't exist yet (fresh install * before {@see \Yatra\Core\Database::createTables()} has run). */ public static function maybeNormalizeAvailabilityRulesLegacyData(): void { if (get_option('yatra_availability_rules_legacy_normalized_v1')) { return; } if (!class_exists('Yatra\\Database\\Tables\\TripAvailabilityRulesTable')) { return; } $table = \Yatra\Database\Tables\TripAvailabilityRulesTable::getTableName(); if (!self::databaseTableExists($table)) { return; } global $wpdb; // 1. Daily-recurrence rows whose `rule_type` defaulted to 'weekly': // map to the new "interval" rule type and copy the legacy `interval` // + `start_date` into the new columns the React form binds to. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper $wpdb->query("UPDATE `{$table}` SET `rule_type` = 'interval', `interval_days` = COALESCE(`interval_days`, NULLIF(`interval`, 0), 1), `interval_start_date` = COALESCE(`interval_start_date`, `start_date`) WHERE `recurrence_type` = 'daily' AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')"); // 2. Monthly-recurrence rows whose `rule_type` defaulted to 'weekly': // relabel to 'monthly'. The new UI uses (week_of_month, day_of_week) // rather than `day_of_month`, so we leave those NULL for the user // to set in the form rather than guess from the legacy day_of_month. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper $wpdb->query("UPDATE `{$table}` SET `rule_type` = 'monthly' WHERE `recurrence_type` = 'monthly' AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')"); // 3. Weekly-recurrence rows: ensure `rule_type` is set explicitly // (most already match the default; this catches any NULL/empty). // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper $wpdb->query("UPDATE `{$table}` SET `rule_type` = 'weekly' WHERE `recurrence_type` = 'weekly' AND (`rule_type` IS NULL OR `rule_type` = '')"); // 4. seats_total backfill from `capacity_value` for fixed-capacity rows // so CapacityService and the React table both surface the right // seat cap without falling through hydrate-time fallbacks. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper $wpdb->query("UPDATE `{$table}` SET `seats_total` = `capacity_value` WHERE `seats_total` IS NULL AND `capacity_value` IS NOT NULL AND `capacity_value` > 0 AND (`capacity_type` IS NULL OR `capacity_type` = 'fixed')"); update_option('yatra_availability_rules_legacy_normalized_v1', '1', false); } /** * Ensure `wp_yatra_bookings.status` accepts `pending_verification`. * * The 3.0.5 guest email-verification feature introduced a new holding * status (`pending_verification`) but the production ENUM only listed * the legacy values. Until the column is widened, MySQL non-strict * mode silently coerces the value to `''` on insert — which makes * every booking placed with verification enabled appear broken: * * 1. The admin booking list renders an empty status badge (the * React status map has no entry for `''` so the row falls into * the default branch with an empty label). * 2. `BookingService::createBooking` sees the in-memory * `$data['status'] === 'pending_verification'` and defers * firing `yatra_booking_created`, so the customer never gets * the booking-confirmation email. * 3. `verify_email` reads the persisted status (now `''`), * decides the booking is "already verified", skips the deferred * fan-out — so neither the status flip nor the booking email * ever fires. * * Doing the widening here (runIdempotentMaintenance — every admin * pageview) instead of a pure version-gated upgrade step means it * heals installs whose stored yatra_version was already bumped to * 3.0.5 by an earlier failed upgrade attempt. Cheap: one * INFORMATION_SCHEMA query gated by a one-shot option flag, ALTER * runs at most once per install. * * Also backfills any rows whose status was silently coerced to `''` * by the pre-widening insert path: those bookings *should* have * landed in `pending_verification`, so we restore them there. The * original verify-email magic link still works because the HMAC * token is bound to booking_id + email, not status. */ public static function maybeAddPendingVerificationBookingStatus(): void { if (get_option('yatra_booking_status_pending_verification_v1')) { return; } if (!class_exists('Yatra\\Database\\Tables\\BookingsTable')) { return; } $table = \Yatra\Database\Tables\BookingsTable::getTableName(); if (!self::databaseTableExists($table)) { return; } global $wpdb; $columnInfo = $wpdb->get_row( $wpdb->prepare( "SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s", DB_NAME, $table, 'status' ) ); $columnType = is_object($columnInfo) ? (string) ($columnInfo->COLUMN_TYPE ?? '') : ''; $needsAlter = $columnType !== '' && strpos($columnType, 'pending_verification') === false; if ($needsAlter) { // Match the original column shape exactly minus the new enum // value — nullable, default 'pending', no NOT NULL. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, enum literal is static. $wpdb->query( 'ALTER TABLE `' . esc_sql($table) . "` " . "MODIFY COLUMN `status` " . "enum('pending','pending_verification','confirmed','processing','completed','cancelled','refunded','failed','on_hold','waitlist') " . "DEFAULT 'pending'" ); } // Backfill: bookings whose insert hit the old ENUM during a // verification flow ended up with status='' (silent coerce). // Now that the enum accepts pending_verification, restore them. // Filtered to a narrow signal (status='' AND payment_status='pending') // so we don't accidentally re-stamp unrelated edge cases. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper, literal values only. $wpdb->query( "UPDATE `{$table}` SET `status` = 'pending_verification' " . "WHERE `status` = '' AND `payment_status` = 'pending'" ); update_option('yatra_booking_status_pending_verification_v1', '1', false); } /** * Add the `duration_hours` column to the trips table for hour-based * (single-day) tours. Purely additive and nullable — existing trips get * NULL and behave exactly as before (day-based via `duration_days`). Only * tours that later set a positive `duration_hours` change behaviour. * * Idempotent: guarded by a one-shot option AND an INFORMATION_SCHEMA check, * so it runs its ALTER at most once and is a no-op when the column already * exists (fresh installs get it from TripsTable::getSchema()). */ public static function maybeAddTripDurationHoursColumn(): void { if (get_option('yatra_trip_duration_hours_v1')) { return; } if (!class_exists('Yatra\\Database\\Tables\\TripsTable')) { return; } $table = \Yatra\Database\Tables\TripsTable::getTableName(); if (!self::databaseTableExists($table)) { return; } global $wpdb; $columnExists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s", DB_NAME, $table, 'duration_hours' ) ); if (!$columnExists) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, static column definition. $wpdb->query( 'ALTER TABLE `' . esc_sql($table) . '` ' . "ADD COLUMN `duration_hours` smallint(5) UNSIGNED DEFAULT NULL " . "COMMENT 'Duration in hours for hour-based (single-day) tours; NULL = day-based' " . 'AFTER `duration_nights`' ); // Re-check so we only mark this done when the column really exists. // If the ALTER failed (e.g. a restrictive host), leave the one-shot // flag unset so it retries on the next admin load rather than // disabling the feature permanently. $columnExists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s", DB_NAME, $table, 'duration_hours' ) ); } if ($columnExists) { update_option('yatra_trip_duration_hours_v1', '1', false); } } /** * Fill canonical + legacy email identity options when empty (upgrades, partial installs, or empty strings in DB). * Idempotent; safe to run on each admin load via maybeBackfillEmailTemplateDefaults(). */ public static function maybeBackfillEmailDeliveryIdentity(): void { $wpAdmin = trim((string) get_option('admin_email', '')); $wpName = trim((string) get_bloginfo('name')); $isUnsetOrEmpty = static function ($v): bool { return $v === false || $v === null || $v === '' || (is_string($v) && trim($v) === ''); }; if ($wpAdmin !== '') { foreach (['yatra_admin_email', 'yatra_from_email', 'yatra_email_from_address'] as $opt) { if ($isUnsetOrEmpty(get_option($opt, false))) { update_option($opt, $wpAdmin); } } } if ($wpName !== '') { foreach (['yatra_from_name', 'yatra_email_from_name'] as $opt) { if ($isUnsetOrEmpty(get_option($opt, false))) { update_option($opt, $wpName); } } } } /** * One-time: persist default HTML subjects/bodies when options exist but are empty (pre-template-defaults installs). */ public static function maybeBackfillEmailTemplateDefaults(): void { self::maybeBackfillEmailDeliveryIdentity(); // Default-on for new option on existing sites (add_option no-ops if already present). add_option('yatra_email_template_admin_new_booking', 1); add_option('yatra_email_template_admin_payment', 1); add_option('yatra_email_template_admin_cancellation', 1); self::maybeBackfillCustomerEmailVerificationTemplate(); self::maybeBackfillExtendedTransactionalEmailOptionsV2(); if (!get_option('yatra_email_identity_synced_v1')) { $from = get_option('yatra_from_email', ''); if (($from === false || $from === '') && ($legacy = get_option('yatra_email_from_address', '')) && is_string($legacy) && $legacy !== '') { update_option('yatra_from_email', $legacy); } $fname = get_option('yatra_from_name', ''); if (($fname === false || $fname === '') && ($legacy = get_option('yatra_email_from_name', '')) && is_string($legacy) && $legacy !== '') { update_option('yatra_from_name', $legacy); } update_option('yatra_email_identity_synced_v1', '1'); } if (get_option('yatra_email_tpl_defaults_backfill_1')) { return; } foreach (EmailTemplateDefaults::settingsOptionDefaults() as $key => $defaultValue) { $name = 'yatra_' . $key; $current = get_option($name, false); $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === ''); if ($isEmpty) { update_option($name, $defaultValue); } } update_option('yatra_email_tpl_defaults_backfill_1', '1'); } /** * One-time: customer email verification template (Email → Templates) for existing installs. */ private static function maybeBackfillCustomerEmailVerificationTemplate(): void { if (get_option('yatra_email_customer_verification_tpl_v1')) { return; } add_option('yatra_email_template_customer_verification', true); add_option('yatra_email_template_guest_verification', true); $defaults = EmailTemplateDefaults::settingsOptionDefaults(); foreach (['email_tpl_customer_verification_subject', 'email_tpl_customer_verification_body'] as $key) { if (!isset($defaults[$key])) { continue; } $name = 'yatra_' . $key; $current = get_option($name, false); $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === ''); if ($isEmpty) { update_option($name, $defaults[$key]); } } update_option('yatra_email_customer_verification_tpl_v1', '1'); } /** * One-time: enable flags + default HTML for extended transactional templates (completed, expiry, scheduled, enquiry, review, abandoned). * Only writes options that are still empty so existing customized HTML in the database is preserved on plugin update. */ private static function maybeBackfillExtendedTransactionalEmailOptionsV2(): void { if (get_option('yatra_email_tpl_extended_v2')) { return; } $boolFlags = [ 'email_template_booking_completed', 'email_template_booking_expired_customer', 'email_template_admin_booking_expired', 'email_template_scheduled_payment_reminder', 'email_template_scheduled_payment_succeeded', 'email_template_scheduled_payment_failed', 'email_template_admin_scheduled_payment_failed', 'email_template_enquiry_received', 'email_template_enquiry_admin', 'email_template_enquiry_response', 'email_template_review_request', 'email_template_abandoned_booking_recovery_first', 'email_template_abandoned_booking_recovery_second', 'email_template_abandoned_booking_recovery_final', ]; foreach ($boolFlags as $flag) { add_option('yatra_' . $flag, true); } $extendedContentKeys = [ 'email_tpl_booking_completed_subject', 'email_tpl_booking_completed_body', 'email_tpl_booking_expired_customer_subject', 'email_tpl_booking_expired_customer_body', 'email_tpl_admin_booking_expired_subject', 'email_tpl_admin_booking_expired_body', 'email_tpl_scheduled_payment_reminder_subject', 'email_tpl_scheduled_payment_reminder_body', 'email_tpl_scheduled_payment_succeeded_subject', 'email_tpl_scheduled_payment_succeeded_body', 'email_tpl_scheduled_payment_failed_subject', 'email_tpl_scheduled_payment_failed_body', 'email_tpl_admin_scheduled_payment_failed_subject', 'email_tpl_admin_scheduled_payment_failed_body', 'email_tpl_enquiry_admin_subject', 'email_tpl_enquiry_admin_body', 'email_tpl_enquiry_received_subject', 'email_tpl_enquiry_received_body', 'email_tpl_enquiry_response_subject', 'email_tpl_enquiry_response_body', 'email_tpl_review_request_subject', 'email_tpl_review_request_body', 'email_tpl_abandoned_booking_recovery_first_subject', 'email_tpl_abandoned_booking_recovery_first_body', 'email_tpl_abandoned_booking_recovery_second_subject', 'email_tpl_abandoned_booking_recovery_second_body', 'email_tpl_abandoned_booking_recovery_final_subject', 'email_tpl_abandoned_booking_recovery_final_body', ]; $defaults = EmailTemplateDefaults::settingsOptionDefaults(); foreach ($extendedContentKeys as $key) { if (!isset($defaults[$key])) { continue; } $name = 'yatra_' . $key; $current = get_option($name, false); $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === ''); if ($isEmpty) { update_option($name, $defaults[$key]); } } update_option('yatra_email_tpl_extended_v2', '1'); } }