PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
yatra / app / Services / InstallerService.php

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

763 lines 32.2 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 // 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
67 // Payment Gateway Settings - Only enable Pay Later by default
68 // These match SettingsService defaults exactly
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);
74 // Set gateway configs with proper structure - only enable pay_later by default
75 $gateway_configs = [
76 'pay_later' => [
77 'enabled' => true,
78 'title' => 'Book Now, Pay Later',
79 'description' => 'Allow customers to reserve now and pay before the trip',
80 ],
81 // Explicitly disable all other gateways
82 'stripe' => [
83 'enabled' => false,
84 'title' => 'Stripe',
85 'description' => 'Accept credit and debit cards',
86 'api_key' => '',
87 'api_secret' => '',
88 'webhook_secret' => '',
89 ],
90 'paypal' => [
91 'enabled' => false,
92 'title' => 'PayPal',
93 'description' => 'Accept PayPal payments',
94 'api_key' => '',
95 'api_secret' => '',
96 ],
97 'razorpay' => [
98 'enabled' => false,
99 'title' => 'Razorpay',
100 'description' => 'Accept payments via Razorpay',
101 'api_key' => '',
102 'api_secret' => '',
103 ],
104 'square' => [
105 'enabled' => false,
106 'title' => 'Square',
107 'description' => 'Accept payments via Square',
108 'api_key' => '',
109 'api_secret' => '',
110 ],
111 'authorize_net' => [
112 'enabled' => false,
113 'title' => 'Authorize.net',
114 'description' => 'Accept payments via Authorize.net',
115 'api_key' => '',
116 'api_secret' => '',
117 ],
118 'bank_transfer' => [
119 'enabled' => false,
120 'title' => 'Bank Transfer',
121 'description' => 'Accept manual bank transfer payments',
122 'api_key' => '',
123 'api_secret' => '',
124 ]
125 ];
126 $seed('yatra_gateway_configs', $gateway_configs);
127 $seed('yatra_gateway_order', []);
128
129 // Currency Settings - Match SettingsService defaults
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);
135
136 // Flexible Payment Settings - Match SettingsService defaults
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);
143
144 $seed('yatra_allow_save_payment_methods', false);
145
146 // Trip Settings - Match SettingsService defaults
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);
152
153 // Customer Settings - Match SettingsService defaults
154 $seed('yatra_enable_customer_accounts', true);
155 $seed('yatra_enable_customer_registration', true);
156
157 // Booking Settings - Match SettingsService defaults
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);
174
175 // Email identity: canonical keys (REST / EmailService) + legacy keys for older code paths
176 $wpAdminEmail = (string) get_option('admin_email', '');
177 $blogName = (string) get_bloginfo('name');
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);
185
186 // Default transactional template HTML + subjects (Email → Templates / settings API)
187 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $optionKey => $value) {
188 $seed('yatra_' . $optionKey, $value);
189 }
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 // Off on a fresh install — see SettingsService::$defaults for why.
211 $seed('yatra_email_template_review_request', false);
212 $seed('yatra_email_template_abandoned_booking_recovery_first', true);
213 $seed('yatra_email_template_abandoned_booking_recovery_second', true);
214 $seed('yatra_email_template_abandoned_booking_recovery_final', true);
215
216 // Clear pre-existing legacy Stripe/PayPal settings, but only on a
217 // brand-new install. On a reactivation these may hold the operator's
218 // configured gateway data, so deleting them would be destructive.
219 if ($isFreshInstall) {
220 delete_option('yatra_stripe_settings');
221 delete_option('yatra_paypal_settings');
222 }
223
224 // Installation tracking: stamp the date once (seed); keep the
225 // version current so upgrade routines can detect version changes.
226 $seed('yatra_installation_date', current_time('mysql'));
227 update_option('yatra_version', defined('YATRA_VERSION') ? YATRA_VERSION : '3.0.3');
228
229
230 }
231
232 /**
233 * Get all required database tables using Table classes
234 *
235 * @return array
236 */
237 public static function getRequiredTables(): array
238 {
239 // Must match \Yatra\Core\Database::createTables() — used for activation, migrations, and targeted checks.
240 $table_classes = [
241 \Yatra\Database\Tables\TripsTable::class,
242 \Yatra\Database\Tables\BookingsTable::class,
243 \Yatra\Database\Tables\BookingPaymentsTable::class,
244 \Yatra\Database\Tables\CustomersTable::class,
245 \Yatra\Database\Tables\BookingTravellersTable::class,
246 \Yatra\Database\Tables\BookingTravellerMetaTable::class,
247 \Yatra\Database\Tables\BookingDeparturesTable::class,
248 \Yatra\Database\Tables\ReviewsTable::class,
249 \Yatra\Database\Tables\DiscountsTable::class,
250 \Yatra\Database\Tables\EnquiriesTable::class,
251 \Yatra\Database\Tables\TripAvailabilityDatesTable::class,
252 \Yatra\Database\Tables\TripAvailabilityRulesTable::class,
253 \Yatra\Database\Tables\TripRevisionsTable::class,
254 \Yatra\Database\Tables\DeparturesTable::class,
255 \Yatra\Database\Tables\TripItineraryDaysTable::class,
256 \Yatra\Database\Tables\TripItineraryDayEntryTable::class,
257 \Yatra\Database\Tables\ClassificationsTable::class,
258 \Yatra\Database\Tables\TripClassificationsTable::class,
259 \Yatra\Database\Tables\TripContentTable::class,
260 ];
261
262 $table_names = [];
263 foreach ($table_classes as $table_class) {
264 if (class_exists($table_class)) {
265 $table_names[] = $table_class::getTableName();
266 }
267 }
268
269 return $table_names;
270 }
271
272 /**
273 * Whether a prefixed table exists. Uses esc_like() because SQL LIKE treats "_" as a wildcard.
274 */
275 public static function databaseTableExists(string $fullTableName): bool
276 {
277 global $wpdb;
278 if ($fullTableName === '') {
279 return false;
280 }
281 $pattern = $wpdb->esc_like($fullTableName);
282 $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $pattern));
283
284 return $found === $fullTableName;
285 }
286
287 /**
288 * Check if this is a fresh installation
289 *
290 * @return bool
291 */
292 public static function isFreshInstallation(): bool
293 {
294 // Check if Yatra version exists in database
295 $installed_version = get_option('yatra_version');
296
297 // If no version is set, it's a fresh installation
298 if ($installed_version === false) {
299 return true;
300 }
301
302 // Check installation date
303 $installation_date = get_option('yatra_installation_date');
304 if ($installation_date === false) {
305 return true;
306 }
307
308 // Additional check: if core tables don't exist, it's fresh
309 $required_tables = self::getRequiredTables();
310 if (!empty($required_tables)) {
311 $trips_table = $required_tables[0]; // Use first table (already has prefix)
312 if (!self::databaseTableExists($trips_table)) {
313 return true;
314 }
315 }
316
317 return false;
318 }
319
320 /**
321 * One-time: coupon migration incorrectly stored status "active"; 3.x uses "publish" (admin + checkout).
322 */
323 public static function maybeNormalizeMigratedCouponDiscountStatuses(): void
324 {
325 if (get_option('yatra_discount_active_status_normalized_v1')) {
326 return;
327 }
328
329 if (!class_exists('Yatra\\Database\\Tables\\DiscountsTable')) {
330 return;
331 }
332
333 $table = \Yatra\Database\Tables\DiscountsTable::getTableName();
334 if (!self::databaseTableExists($table)) {
335 return;
336 }
337
338 global $wpdb;
339 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
340 $wpdb->query("UPDATE `{$table}` SET `status` = 'publish' WHERE `status` = 'active'");
341
342 update_option('yatra_discount_active_status_normalized_v1', '1', false);
343 }
344
345 /**
346 * One-time normalization of recurring availability rules created against the
347 * legacy schema (only `recurrence_type`, `capacity_value`, `interval`, etc.
348 * were written) so the new admin React UI — which reads `rule_type`,
349 * `seats_total`, `interval_days`, `interval_start_date` — can render and
350 * edit them without showing phantom "1 on All / Active" badges or empty
351 * "Every " patterns.
352 *
353 * What this fixes:
354 * - Sample-data and pre-3.x rows landed with `rule_type` defaulted to
355 * 'weekly' regardless of the actual `recurrence_type`, and with
356 * `seats_total` left NULL (the new capacity column). Daily and monthly
357 * rules therefore appeared as broken weekly rows in the new UI.
358 * - The /counts endpoint correctly reported 1 active rule, but the list
359 * table couldn't render it cleanly, leading users to read the API
360 * response as "ghost data".
361 *
362 * Invariants:
363 * - Idempotent — every UPDATE filters rows whose new columns are still
364 * unset, so re-running is a no-op once the data is healed.
365 * - Read-only on rows already authored by the new UI (`rule_type` already
366 * matches the recurrence intent), so user edits are never overwritten.
367 * - No-ops cleanly when the rules table doesn't exist yet (fresh install
368 * before {@see \Yatra\Core\Database::createTables()} has run).
369 */
370 public static function maybeNormalizeAvailabilityRulesLegacyData(): void
371 {
372 if (get_option('yatra_availability_rules_legacy_normalized_v1')) {
373 return;
374 }
375
376 if (!class_exists('Yatra\\Database\\Tables\\TripAvailabilityRulesTable')) {
377 return;
378 }
379
380 $table = \Yatra\Database\Tables\TripAvailabilityRulesTable::getTableName();
381 if (!self::databaseTableExists($table)) {
382 return;
383 }
384
385 global $wpdb;
386
387 // 1. Daily-recurrence rows whose `rule_type` defaulted to 'weekly':
388 // map to the new "interval" rule type and copy the legacy `interval`
389 // + `start_date` into the new columns the React form binds to.
390 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
391 $wpdb->query("UPDATE `{$table}`
392 SET `rule_type` = 'interval',
393 `interval_days` = COALESCE(`interval_days`, NULLIF(`interval`, 0), 1),
394 `interval_start_date` = COALESCE(`interval_start_date`, `start_date`)
395 WHERE `recurrence_type` = 'daily'
396 AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
397
398 // 2. Monthly-recurrence rows whose `rule_type` defaulted to 'weekly':
399 // relabel to 'monthly'. The new UI uses (week_of_month, day_of_week)
400 // rather than `day_of_month`, so we leave those NULL for the user
401 // to set in the form rather than guess from the legacy day_of_month.
402 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
403 $wpdb->query("UPDATE `{$table}`
404 SET `rule_type` = 'monthly'
405 WHERE `recurrence_type` = 'monthly'
406 AND (`rule_type` IS NULL OR `rule_type` = '' OR `rule_type` = 'weekly')");
407
408 // 3. Weekly-recurrence rows: ensure `rule_type` is set explicitly
409 // (most already match the default; this catches any NULL/empty).
410 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
411 $wpdb->query("UPDATE `{$table}`
412 SET `rule_type` = 'weekly'
413 WHERE `recurrence_type` = 'weekly'
414 AND (`rule_type` IS NULL OR `rule_type` = '')");
415
416 // 4. seats_total backfill from `capacity_value` for fixed-capacity rows
417 // so CapacityService and the React table both surface the right
418 // seat cap without falling through hydrate-time fallbacks.
419 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
420 $wpdb->query("UPDATE `{$table}`
421 SET `seats_total` = `capacity_value`
422 WHERE `seats_total` IS NULL
423 AND `capacity_value` IS NOT NULL
424 AND `capacity_value` > 0
425 AND (`capacity_type` IS NULL OR `capacity_type` = 'fixed')");
426
427 update_option('yatra_availability_rules_legacy_normalized_v1', '1', false);
428 }
429
430 /**
431 * Ensure `wp_yatra_bookings.status` accepts `pending_verification`.
432 *
433 * The 3.0.5 guest email-verification feature introduced a new holding
434 * status (`pending_verification`) but the production ENUM only listed
435 * the legacy values. Until the column is widened, MySQL non-strict
436 * mode silently coerces the value to `''` on insert — which makes
437 * every booking placed with verification enabled appear broken:
438 *
439 * 1. The admin booking list renders an empty status badge (the
440 * React status map has no entry for `''` so the row falls into
441 * the default branch with an empty label).
442 * 2. `BookingService::createBooking` sees the in-memory
443 * `$data['status'] === 'pending_verification'` and defers
444 * firing `yatra_booking_created`, so the customer never gets
445 * the booking-confirmation email.
446 * 3. `verify_email` reads the persisted status (now `''`),
447 * decides the booking is "already verified", skips the deferred
448 * fan-out — so neither the status flip nor the booking email
449 * ever fires.
450 *
451 * Doing the widening here (runIdempotentMaintenance — every admin
452 * pageview) instead of a pure version-gated upgrade step means it
453 * heals installs whose stored yatra_version was already bumped to
454 * 3.0.5 by an earlier failed upgrade attempt. Cheap: one
455 * INFORMATION_SCHEMA query gated by a one-shot option flag, ALTER
456 * runs at most once per install.
457 *
458 * Also backfills any rows whose status was silently coerced to `''`
459 * by the pre-widening insert path: those bookings *should* have
460 * landed in `pending_verification`, so we restore them there. The
461 * original verify-email magic link still works because the HMAC
462 * token is bound to booking_id + email, not status.
463 */
464 public static function maybeAddPendingVerificationBookingStatus(): void
465 {
466 if (get_option('yatra_booking_status_pending_verification_v1')) {
467 return;
468 }
469
470 if (!class_exists('Yatra\\Database\\Tables\\BookingsTable')) {
471 return;
472 }
473
474 $table = \Yatra\Database\Tables\BookingsTable::getTableName();
475 if (!self::databaseTableExists($table)) {
476 return;
477 }
478
479 global $wpdb;
480
481 $columnInfo = $wpdb->get_row(
482 $wpdb->prepare(
483 "SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
484 WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
485 DB_NAME,
486 $table,
487 'status'
488 )
489 );
490
491 $columnType = is_object($columnInfo) ? (string) ($columnInfo->COLUMN_TYPE ?? '') : '';
492 $needsAlter = $columnType !== '' && strpos($columnType, 'pending_verification') === false;
493
494 if ($needsAlter) {
495 // Match the original column shape exactly minus the new enum
496 // value — nullable, default 'pending', no NOT NULL.
497 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, enum literal is static.
498 $wpdb->query(
499 'ALTER TABLE `' . esc_sql($table) . "` "
500 . "MODIFY COLUMN `status` "
501 . "enum('pending','pending_verification','confirmed','processing','completed','cancelled','refunded','failed','on_hold','waitlist') "
502 . "DEFAULT 'pending'"
503 );
504 }
505
506 // Backfill: bookings whose insert hit the old ENUM during a
507 // verification flow ended up with status='' (silent coerce).
508 // Now that the enum accepts pending_verification, restore them.
509 // Filtered to a narrow signal (status='' AND payment_status='pending')
510 // so we don't accidentally re-stamp unrelated edge cases.
511 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper, literal values only.
512 $wpdb->query(
513 "UPDATE `{$table}` SET `status` = 'pending_verification' "
514 . "WHERE `status` = '' AND `payment_status` = 'pending'"
515 );
516
517 update_option('yatra_booking_status_pending_verification_v1', '1', false);
518 }
519
520 /**
521 * Add the `duration_hours` column to the trips table for hour-based
522 * (single-day) tours. Purely additive and nullable — existing trips get
523 * NULL and behave exactly as before (day-based via `duration_days`). Only
524 * tours that later set a positive `duration_hours` change behaviour.
525 *
526 * Idempotent: guarded by a one-shot option AND an INFORMATION_SCHEMA check,
527 * so it runs its ALTER at most once and is a no-op when the column already
528 * exists (fresh installs get it from TripsTable::getSchema()).
529 */
530 public static function maybeAddTripDurationHoursColumn(): void
531 {
532 if (get_option('yatra_trip_duration_hours_v1')) {
533 return;
534 }
535
536 if (!class_exists('Yatra\\Database\\Tables\\TripsTable')) {
537 return;
538 }
539
540 $table = \Yatra\Database\Tables\TripsTable::getTableName();
541 if (!self::databaseTableExists($table)) {
542 return;
543 }
544
545 global $wpdb;
546
547 $columnExists = $wpdb->get_var(
548 $wpdb->prepare(
549 "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
550 WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
551 DB_NAME,
552 $table,
553 'duration_hours'
554 )
555 );
556
557 if (!$columnExists) {
558 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name escaped, static column definition.
559 $wpdb->query(
560 'ALTER TABLE `' . esc_sql($table) . '` '
561 . "ADD COLUMN `duration_hours` smallint(5) UNSIGNED DEFAULT NULL "
562 . "COMMENT 'Duration in hours for hour-based single-day tours, NULL means day-based' "
563 . 'AFTER `duration_nights`'
564 );
565
566 // Re-check so we only mark this done when the column really exists.
567 // If the ALTER failed (e.g. a restrictive host), leave the one-shot
568 // flag unset so it retries on the next admin load rather than
569 // disabling the feature permanently.
570 $columnExists = $wpdb->get_var(
571 $wpdb->prepare(
572 "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
573 WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
574 DB_NAME,
575 $table,
576 'duration_hours'
577 )
578 );
579 }
580
581 if ($columnExists) {
582 update_option('yatra_trip_duration_hours_v1', '1', false);
583 }
584 }
585
586 /**
587 * Fill canonical + legacy email identity options when empty (upgrades, partial installs, or empty strings in DB).
588 * Idempotent; safe to run on each admin load via maybeBackfillEmailTemplateDefaults().
589 */
590 public static function maybeBackfillEmailDeliveryIdentity(): void
591 {
592 $wpAdmin = trim((string) get_option('admin_email', ''));
593 $wpName = trim((string) get_bloginfo('name'));
594
595 $isUnsetOrEmpty = static function ($v): bool {
596 return $v === false || $v === null || $v === '' || (is_string($v) && trim($v) === '');
597 };
598
599 if ($wpAdmin !== '') {
600 foreach (['yatra_admin_email', 'yatra_from_email', 'yatra_email_from_address'] as $opt) {
601 if ($isUnsetOrEmpty(get_option($opt, false))) {
602 update_option($opt, $wpAdmin);
603 }
604 }
605 }
606 if ($wpName !== '') {
607 foreach (['yatra_from_name', 'yatra_email_from_name'] as $opt) {
608 if ($isUnsetOrEmpty(get_option($opt, false))) {
609 update_option($opt, $wpName);
610 }
611 }
612 }
613 }
614
615 /**
616 * One-time: persist default HTML subjects/bodies when options exist but are empty (pre-template-defaults installs).
617 */
618 public static function maybeBackfillEmailTemplateDefaults(): void
619 {
620 self::maybeBackfillEmailDeliveryIdentity();
621
622 // Default-on for new option on existing sites (add_option no-ops if already present).
623 add_option('yatra_email_template_admin_new_booking', 1);
624 add_option('yatra_email_template_admin_payment', 1);
625 add_option('yatra_email_template_admin_cancellation', 1);
626
627 self::maybeBackfillCustomerEmailVerificationTemplate();
628 self::maybeBackfillExtendedTransactionalEmailOptionsV2();
629
630 if (!get_option('yatra_email_identity_synced_v1')) {
631 $from = get_option('yatra_from_email', '');
632 if (($from === false || $from === '') && ($legacy = get_option('yatra_email_from_address', '')) && is_string($legacy) && $legacy !== '') {
633 update_option('yatra_from_email', $legacy);
634 }
635 $fname = get_option('yatra_from_name', '');
636 if (($fname === false || $fname === '') && ($legacy = get_option('yatra_email_from_name', '')) && is_string($legacy) && $legacy !== '') {
637 update_option('yatra_from_name', $legacy);
638 }
639 update_option('yatra_email_identity_synced_v1', '1');
640 }
641
642 if (get_option('yatra_email_tpl_defaults_backfill_1')) {
643 return;
644 }
645
646 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $key => $defaultValue) {
647 $name = 'yatra_' . $key;
648 $current = get_option($name, false);
649 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
650 if ($isEmpty) {
651 update_option($name, $defaultValue);
652 }
653 }
654
655 update_option('yatra_email_tpl_defaults_backfill_1', '1');
656 }
657
658 /**
659 * One-time: customer email verification template (Email → Templates) for existing installs.
660 */
661 private static function maybeBackfillCustomerEmailVerificationTemplate(): void
662 {
663 if (get_option('yatra_email_customer_verification_tpl_v1')) {
664 return;
665 }
666
667 add_option('yatra_email_template_customer_verification', true);
668 add_option('yatra_email_template_guest_verification', true);
669
670 $defaults = EmailTemplateDefaults::settingsOptionDefaults();
671 foreach (['email_tpl_customer_verification_subject', 'email_tpl_customer_verification_body'] as $key) {
672 if (!isset($defaults[$key])) {
673 continue;
674 }
675 $name = 'yatra_' . $key;
676 $current = get_option($name, false);
677 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
678 if ($isEmpty) {
679 update_option($name, $defaults[$key]);
680 }
681 }
682
683 update_option('yatra_email_customer_verification_tpl_v1', '1');
684 }
685
686 /**
687 * One-time: enable flags + default HTML for extended transactional templates (completed, expiry, scheduled, enquiry, review, abandoned).
688 * Only writes options that are still empty so existing customized HTML in the database is preserved on plugin update.
689 */
690 private static function maybeBackfillExtendedTransactionalEmailOptionsV2(): void
691 {
692 if (get_option('yatra_email_tpl_extended_v2')) {
693 return;
694 }
695
696 $boolFlags = [
697 'email_template_booking_completed',
698 'email_template_booking_expired_customer',
699 'email_template_admin_booking_expired',
700 'email_template_scheduled_payment_reminder',
701 'email_template_scheduled_payment_succeeded',
702 'email_template_scheduled_payment_failed',
703 'email_template_admin_scheduled_payment_failed',
704 'email_template_enquiry_received',
705 'email_template_enquiry_admin',
706 'email_template_enquiry_response',
707 'email_template_review_request',
708 'email_template_abandoned_booking_recovery_first',
709 'email_template_abandoned_booking_recovery_second',
710 'email_template_abandoned_booking_recovery_final',
711 ];
712 foreach ($boolFlags as $flag) {
713 add_option('yatra_' . $flag, true);
714 }
715
716 $extendedContentKeys = [
717 'email_tpl_booking_completed_subject',
718 'email_tpl_booking_completed_body',
719 'email_tpl_booking_expired_customer_subject',
720 'email_tpl_booking_expired_customer_body',
721 'email_tpl_admin_booking_expired_subject',
722 'email_tpl_admin_booking_expired_body',
723 'email_tpl_scheduled_payment_reminder_subject',
724 'email_tpl_scheduled_payment_reminder_body',
725 'email_tpl_scheduled_payment_succeeded_subject',
726 'email_tpl_scheduled_payment_succeeded_body',
727 'email_tpl_scheduled_payment_failed_subject',
728 'email_tpl_scheduled_payment_failed_body',
729 'email_tpl_admin_scheduled_payment_failed_subject',
730 'email_tpl_admin_scheduled_payment_failed_body',
731 'email_tpl_enquiry_admin_subject',
732 'email_tpl_enquiry_admin_body',
733 'email_tpl_enquiry_received_subject',
734 'email_tpl_enquiry_received_body',
735 'email_tpl_enquiry_response_subject',
736 'email_tpl_enquiry_response_body',
737 'email_tpl_review_request_subject',
738 'email_tpl_review_request_body',
739 'email_tpl_abandoned_booking_recovery_first_subject',
740 'email_tpl_abandoned_booking_recovery_first_body',
741 'email_tpl_abandoned_booking_recovery_second_subject',
742 'email_tpl_abandoned_booking_recovery_second_body',
743 'email_tpl_abandoned_booking_recovery_final_subject',
744 'email_tpl_abandoned_booking_recovery_final_body',
745 ];
746
747 $defaults = EmailTemplateDefaults::settingsOptionDefaults();
748 foreach ($extendedContentKeys as $key) {
749 if (!isset($defaults[$key])) {
750 continue;
751 }
752 $name = 'yatra_' . $key;
753 $current = get_option($name, false);
754 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
755 if ($isEmpty) {
756 update_option($name, $defaults[$key]);
757 }
758 }
759
760 update_option('yatra_email_tpl_extended_v2', '1');
761 }
762 }
763