PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Services / InstallerService.php

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

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