PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.11 All 82 releases
yatra / app / Services / InstallerService.php

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

762 lines 32.1 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 $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 }
222
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');
227
228
229 }
230
231 /**
232 * Get all required database tables using Table classes
233 *
234 * @return array
235 */
236 public static function getRequiredTables(): array
237 {
238 // Must match \Yatra\Core\Database::createTables() — used for activation, migrations, and targeted checks.
239 $table_classes = [
240 \Yatra\Database\Tables\TripsTable::class,
241 \Yatra\Database\Tables\BookingsTable::class,
242 \Yatra\Database\Tables\BookingPaymentsTable::class,
243 \Yatra\Database\Tables\CustomersTable::class,
244 \Yatra\Database\Tables\BookingTravellersTable::class,
245 \Yatra\Database\Tables\BookingTravellerMetaTable::class,
246 \Yatra\Database\Tables\BookingDeparturesTable::class,
247 \Yatra\Database\Tables\ReviewsTable::class,
248 \Yatra\Database\Tables\DiscountsTable::class,
249 \Yatra\Database\Tables\EnquiriesTable::class,
250 \Yatra\Database\Tables\TripAvailabilityDatesTable::class,
251 \Yatra\Database\Tables\TripAvailabilityRulesTable::class,
252 \Yatra\Database\Tables\TripRevisionsTable::class,
253 \Yatra\Database\Tables\DeparturesTable::class,
254 \Yatra\Database\Tables\TripItineraryDaysTable::class,
255 \Yatra\Database\Tables\TripItineraryDayEntryTable::class,
256 \Yatra\Database\Tables\ClassificationsTable::class,
257 \Yatra\Database\Tables\TripClassificationsTable::class,
258 \Yatra\Database\Tables\TripContentTable::class,
259 ];
260
261 $table_names = [];
262 foreach ($table_classes as $table_class) {
263 if (class_exists($table_class)) {
264 $table_names[] = $table_class::getTableName();
265 }
266 }
267
268 return $table_names;
269 }
270
271 /**
272 * Whether a prefixed table exists. Uses esc_like() because SQL LIKE treats "_" as a wildcard.
273 */
274 public static function databaseTableExists(string $fullTableName): bool
275 {
276 global $wpdb;
277 if ($fullTableName === '') {
278 return false;
279 }
280 $pattern = $wpdb->esc_like($fullTableName);
281 $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $pattern));
282
283 return $found === $fullTableName;
284 }
285
286 /**
287 * Check if this is a fresh installation
288 *
289 * @return bool
290 */
291 public static function isFreshInstallation(): bool
292 {
293 // Check if Yatra version exists in database
294 $installed_version = get_option('yatra_version');
295
296 // If no version is set, it's a fresh installation
297 if ($installed_version === false) {
298 return true;
299 }
300
301 // Check installation date
302 $installation_date = get_option('yatra_installation_date');
303 if ($installation_date === false) {
304 return true;
305 }
306
307 // Additional check: if core tables don't exist, it's fresh
308 $required_tables = self::getRequiredTables();
309 if (!empty($required_tables)) {
310 $trips_table = $required_tables[0]; // Use first table (already has prefix)
311 if (!self::databaseTableExists($trips_table)) {
312 return true;
313 }
314 }
315
316 return false;
317 }
318
319 /**
320 * One-time: coupon migration incorrectly stored status "active"; 3.x uses "publish" (admin + checkout).
321 */
322 public static function maybeNormalizeMigratedCouponDiscountStatuses(): void
323 {
324 if (get_option('yatra_discount_active_status_normalized_v1')) {
325 return;
326 }
327
328 if (!class_exists('Yatra\\Database\\Tables\\DiscountsTable')) {
329 return;
330 }
331
332 $table = \Yatra\Database\Tables\DiscountsTable::getTableName();
333 if (!self::databaseTableExists($table)) {
334 return;
335 }
336
337 global $wpdb;
338 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from schema helper
339 $wpdb->query("UPDATE `{$table}` SET `status` = 'publish' WHERE `status` = 'active'");
340
341 update_option('yatra_discount_active_status_normalized_v1', '1', false);
342 }
343
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 = 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 /**
586 * Fill canonical + legacy email identity options when empty (upgrades, partial installs, or empty strings in DB).
587 * Idempotent; safe to run on each admin load via maybeBackfillEmailTemplateDefaults().
588 */
589 public static function maybeBackfillEmailDeliveryIdentity(): void
590 {
591 $wpAdmin = trim((string) get_option('admin_email', ''));
592 $wpName = trim((string) get_bloginfo('name'));
593
594 $isUnsetOrEmpty = static function ($v): bool {
595 return $v === false || $v === null || $v === '' || (is_string($v) && trim($v) === '');
596 };
597
598 if ($wpAdmin !== '') {
599 foreach (['yatra_admin_email', 'yatra_from_email', 'yatra_email_from_address'] as $opt) {
600 if ($isUnsetOrEmpty(get_option($opt, false))) {
601 update_option($opt, $wpAdmin);
602 }
603 }
604 }
605 if ($wpName !== '') {
606 foreach (['yatra_from_name', 'yatra_email_from_name'] as $opt) {
607 if ($isUnsetOrEmpty(get_option($opt, false))) {
608 update_option($opt, $wpName);
609 }
610 }
611 }
612 }
613
614 /**
615 * One-time: persist default HTML subjects/bodies when options exist but are empty (pre-template-defaults installs).
616 */
617 public static function maybeBackfillEmailTemplateDefaults(): void
618 {
619 self::maybeBackfillEmailDeliveryIdentity();
620
621 // Default-on for new option on existing sites (add_option no-ops if already present).
622 add_option('yatra_email_template_admin_new_booking', 1);
623 add_option('yatra_email_template_admin_payment', 1);
624 add_option('yatra_email_template_admin_cancellation', 1);
625
626 self::maybeBackfillCustomerEmailVerificationTemplate();
627 self::maybeBackfillExtendedTransactionalEmailOptionsV2();
628
629 if (!get_option('yatra_email_identity_synced_v1')) {
630 $from = get_option('yatra_from_email', '');
631 if (($from === false || $from === '') && ($legacy = get_option('yatra_email_from_address', '')) && is_string($legacy) && $legacy !== '') {
632 update_option('yatra_from_email', $legacy);
633 }
634 $fname = get_option('yatra_from_name', '');
635 if (($fname === false || $fname === '') && ($legacy = get_option('yatra_email_from_name', '')) && is_string($legacy) && $legacy !== '') {
636 update_option('yatra_from_name', $legacy);
637 }
638 update_option('yatra_email_identity_synced_v1', '1');
639 }
640
641 if (get_option('yatra_email_tpl_defaults_backfill_1')) {
642 return;
643 }
644
645 foreach (EmailTemplateDefaults::settingsOptionDefaults() as $key => $defaultValue) {
646 $name = 'yatra_' . $key;
647 $current = get_option($name, false);
648 $isEmpty = $current === false || $current === '' || (is_string($current) && trim($current) === '');
649 if ($isEmpty) {
650 update_option($name, $defaultValue);
651 }
652 }
653
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');
760 }
761 }
762