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