| @@ -6,11 +6,8 @@ | ||
| 6 | 6 | use FluentCart\App\Models\Product; |
| 7 | 7 | |
| 8 | 8 | require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| 9 | 9 | |
| 10 | -use FluentCart\Database\Migrations\AttributeGroupsMigrator; | |
| 11 | -use FluentCart\Database\Migrations\AttributeObjectRelationsMigrator; | |
| 12 | -use FluentCart\Database\Migrations\AttributeTermsMigrator; | |
| 13 | 10 | use FluentCart\Database\Migrations\CartMigrator; |
| 14 | 11 | use FluentCart\Database\Migrations\CustomersMigrator; |
| 15 | 12 | use FluentCart\Database\Migrations\MetaMigrator; |
| 16 | 13 | use FluentCart\Database\Migrations\Migrator; |
| @@ -20,8 +17,9 @@ | ||
| 20 | 17 | use FluentCart\Database\Migrations\OrderTransactionsMigrator; |
| 21 | 18 | use FluentCart\Database\Migrations\ProductDetailsMigrator; |
| 22 | 19 | use FluentCart\Database\Migrations\ProductDownloadsMigrator; |
| 23 | 20 | use FluentCart\Database\Migrations\ProductMetaMigrator; |
| 21 | +use FluentCart\Database\Migrations\ProductReviewsMigrator; | |
| 24 | 22 | use FluentCart\Database\Migrations\ProductVariationMigrator; |
| 25 | 23 | use FluentCart\Database\Migrations\ScheduledActionsMigrator; |
| 26 | 24 | use FluentCart\Database\Migrations\ShippingClassesMigrator; |
| 27 | 25 | use FluentCart\Database\Migrations\SubscriptionMetaMigrator; |
| @@ -47,16 +45,17 @@ | ||
| 47 | 45 | use FluentCartPro\App\Modules\Licensing\Models\LicenseSite; |
| 48 | 46 | use FluentCart\Database\Migrations\ShippingZonesMigrator; |
| 49 | 47 | use FluentCart\Database\Migrations\ShippingMethodsMigrator; |
| 50 | 48 | use FluentCart\Database\Migrations\RetentionSnapshotsMigrator; |
| 49 | +use FluentCart\Database\Migrations\AttributeGroupsMigrator; | |
| 50 | +use FluentCart\Database\Migrations\AttributeObjectRelationsMigrator; | |
| 51 | +use FluentCart\Database\Migrations\AttributeTermsMigrator; | |
| 52 | +use FluentCart\Database\Seeder\AttributeSeeder; | |
| 51 | 53 | |
| 52 | 54 | class DBMigrator |
| 53 | 55 | { |
| 54 | 56 | private static array $migrators = [ |
| 55 | 57 | MetaMigrator::class, |
| 56 | - AttributeGroupsMigrator::class, | |
| 57 | - AttributeObjectRelationsMigrator::class, | |
| 58 | - AttributeTermsMigrator::class, | |
| 59 | 58 | CartMigrator::class, |
| 60 | 59 | CouponsMigrator::class, |
| 61 | 60 | CustomerAddressesMigrator::class, |
| 62 | 61 | CustomerMetaMigrator::class, |
| @@ -71,8 +70,9 @@ | ||
| 71 | 70 | OrderTransactionsMigrator::class, |
| 72 | 71 | ProductDetailsMigrator::class, |
| 73 | 72 | ProductDownloadsMigrator::class, |
| 74 | 73 | ProductMetaMigrator::class, |
| 74 | + ProductReviewsMigrator::class, | |
| 75 | 75 | SubscriptionMetaMigrator::class, |
| 76 | 76 | SubscriptionsMigrator::class, |
| 77 | 77 | TaxClassesMigrator::class, |
| 78 | 78 | TaxRatesMigrator::class, |
| @@ -85,9 +85,15 @@ | ||
| 85 | 85 | ShippingZonesMigrator::class, |
| 86 | 86 | ShippingMethodsMigrator::class, |
| 87 | 87 | ShippingClassesMigrator::class, |
| 88 | 88 | ScheduledActionsMigrator::class, |
| 89 | - RetentionSnapshotsMigrator::class | |
| 89 | + RetentionSnapshotsMigrator::class, | |
| 90 | + // Order matters: AttributeTermsMigrator::migrated() INNER JOINs | |
| 91 | + // fct_atts_relations to repoint orphaned term references, so the | |
| 92 | + // relations table must exist before terms runs its post-migration hook. | |
| 93 | + AttributeGroupsMigrator::class, | |
| 94 | + AttributeObjectRelationsMigrator::class, | |
| 95 | + AttributeTermsMigrator::class | |
| 90 | 96 | ]; |
| 91 | 97 | |
| 92 | 98 | public static function migrateUp($network_wide = false) |
| 93 | 99 | { |
| @@ -125,12 +131,16 @@ | ||
| 125 | 131 | */ |
| 126 | 132 | foreach (self::$migrators as $migrator) { |
| 127 | 133 | $migrator::migrate(); |
| 128 | 134 | } |
| 135 | + | |
| 136 | + do_action('fluent_cart/after_migrate'); | |
| 129 | 137 | } |
| 130 | 138 | |
| 131 | 139 | public static function maybeMigrateDBChanges() |
| 132 | 140 | { |
| 141 | + self::maybeMigrateCustomerRecoveryIndexes(); | |
| 142 | + | |
| 133 | 143 | /* |
| 134 | 144 | * TODO We will remove this after final release |
| 135 | 145 | */ |
| 136 | 146 | $currentDBVersion = get_option('_fluent_cart_db_version'); |
| @@ -148,18 +158,38 @@ | ||
| 148 | 158 | } |
| 149 | 159 | |
| 150 | 160 | update_option('_fluent_cart_db_version', FLUENTCART_DB_VERSION, 'no'); |
| 151 | 161 | |
| 162 | + if (!$currentDBVersion) { | |
| 163 | + // Brand-new store: default tax display to the simplified single line. | |
| 164 | + // Existing stores fall through to the read-time default 'itemized'. | |
| 165 | + $taxSettings = get_option('fluent_cart_tax_configuration_settings', []); | |
| 166 | + if (!isset($taxSettings['checkout_tax_breakdown_display'])) { | |
| 167 | + $taxSettings['checkout_tax_breakdown_display'] = 'simplified'; | |
| 168 | + update_option('fluent_cart_tax_configuration_settings', $taxSettings, true); | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 152 | 172 | // 2026-04-25 |
| 153 | 173 | TaxRatesMigrator::upgradeShippingOverridePrecision(); |
| 154 | 174 | |
| 175 | + // 2026-07-01 | |
| 176 | + OrderTaxRateMigrator::allowVirtualTaxRateIds(); | |
| 177 | + | |
| 155 | 178 | // 2026-05-27 |
| 156 | 179 | TaxRatesMigrator::fixPostcodeRangeSeparator(); |
| 157 | 180 | OrdersMigrator::addFeeTotalColumn(); |
| 158 | 181 | |
| 159 | - // 2026-05-10 | |
| 160 | - AttributeGroupsMigrator::dropLegacyTitleUniqueIndexes(); | |
| 182 | + // 2026-07-24 | |
| 183 | + OrdersMigrator::addUuidIndex(); | |
| 161 | 184 | |
| 185 | + // 2026-08-20 | |
| 186 | + ProductMetaMigrator::addObjectMetaIndex(); | |
| 187 | + | |
| 188 | + // 2026-09-07 — product reviews. A store updating from a release | |
| 189 | + // without the reviews table gets it created here. | |
| 190 | + ProductReviewsMigrator::migrate(); | |
| 191 | + | |
| 162 | 192 | // let's check the orders table sequence number |
| 163 | 193 | global $wpdb; |
| 164 | 194 | |
| 165 | 195 | // Product Meta unique index removal |
| @@ -461,9 +491,15 @@ | ||
| 461 | 491 | \FluentCart\Database\Migrations\ShippingZonesMigrator::migrated(); |
| 462 | 492 | \FluentCart\Database\Migrations\ShippingClassesMigrator::migrated(); |
| 463 | 493 | \FluentCart\Database\Migrations\SubscriptionsMigrator::migrated(); |
| 464 | 494 | |
| 495 | + // 2026-08-06 | |
| 496 | + // fct_order_transactions subscription_id index — also run here so | |
| 497 | + // in-place plugin updates deliver it; migrate() only covers the | |
| 498 | + // activation path. | |
| 499 | + \FluentCart\Database\Migrations\OrderTransactionsMigrator::migrated(); | |
| 465 | 500 | |
| 501 | + | |
| 466 | 502 | // 2026-05-06 |
| 467 | 503 | // Backfill, dedup, then add unique index — must run in this order |
| 468 | 504 | // so the constraint can be applied without "Duplicate entry" errors. |
| 469 | 505 | TaxClassesMigrator::backfillNullSlugs(); |
| @@ -497,8 +533,21 @@ | ||
| 497 | 533 | } |
| 498 | 534 | update_option('_fluent_cart_price_suffix_migrated', '1', 'no'); |
| 499 | 535 | } |
| 500 | 536 | |
| 537 | + // 2026-07-08 | |
| 538 | + // One-time migration: rename the legacy tax-display value 'both' | |
| 539 | + // (and the removed 'label'/'tooltip') to 'itemized'. Value-guarded — | |
| 540 | + // once migrated the stored value is 'itemized' so this no-ops; no | |
| 541 | + // separate flag option is written (nothing left behind when this | |
| 542 | + // block is removed later). Safe to remove after ~2027-07. | |
| 543 | + $fctTaxSettings = get_option('fluent_cart_tax_configuration_settings', []); | |
| 544 | + if (isset($fctTaxSettings['checkout_tax_breakdown_display']) | |
| 545 | + && in_array($fctTaxSettings['checkout_tax_breakdown_display'], ['both', 'label', 'tooltip'], true)) { | |
| 546 | + $fctTaxSettings['checkout_tax_breakdown_display'] = 'itemized'; | |
| 547 | + update_option('fluent_cart_tax_configuration_settings', $fctTaxSettings, true); | |
| 548 | + } | |
| 549 | + | |
| 501 | 550 | // 2026-05-18 |
| 502 | 551 | // One-time migration: copy seller identity fields from the Pro seller_details fct_meta |
| 503 | 552 | // entry into fluent_cart_store_settings. Guarded by a sentinel so it runs exactly once. |
| 504 | 553 | if (!get_option('_fluent_cart_seller_details_migrated')) { |
| @@ -540,8 +589,54 @@ | ||
| 540 | 589 | $table_name |
| 541 | 590 | )); |
| 542 | 591 | } |
| 543 | 592 | |
| 593 | + // 2026-06-17 | |
| 594 | + // Advanced Variation - Create the attribute | |
| 595 | + // schema (and seed the system-template groups) on in-place plugin | |
| 596 | + // updates too — migrate() only runs on activation, but existing | |
| 597 | + // installs reach this version-gated path without reactivating. Each | |
| 598 | + // migrator is idempotent (dbDelta) and the seeder early-returns when | |
| 599 | + // any group already exists, so re-runs never duplicate data. Order | |
| 600 | + // matters: relations table must exist before AttributeTermsMigrator's | |
| 601 | + // post-migration JOIN cleanup. | |
| 602 | + AttributeGroupsMigrator::migrate(); | |
| 603 | + AttributeObjectRelationsMigrator::migrate(); | |
| 604 | + AttributeTermsMigrator::migrate(); | |
| 605 | + AttributeSeeder::seed(); | |
| 606 | + | |
| 607 | + self::releaseMigrationLock(); | |
| 608 | + } | |
| 609 | + } | |
| 610 | + | |
| 611 | + /** Upgrade active stores without running index DDL on customer requests. */ | |
| 612 | + public static function maybeMigrateCustomerRecoveryIndexes(): void | |
| 613 | + { | |
| 614 | + $option = '_fluent_cart_customer_recovery_indexes_version'; | |
| 615 | + if (get_option($option) === '1') { | |
| 616 | + return; | |
| 617 | + } | |
| 618 | + if (!(defined('WP_CLI') && WP_CLI) && (!is_admin() || !current_user_can('manage_options'))) { | |
| 619 | + return; | |
| 620 | + } | |
| 621 | + if (!self::acquireMigrationLock()) { | |
| 622 | + return; | |
| 623 | + } | |
| 624 | + try { | |
| 625 | + if (get_option($option) === '1') { | |
| 626 | + return; | |
| 627 | + } | |
| 628 | + foreach ([SubscriptionsMigrator::class, OrderDownloadPermissionsMigrator::class, CartMigrator::class] as $migrator) { | |
| 629 | + if (!Schema::hasTable($migrator::$tableName)) { | |
| 630 | + return; | |
| 631 | + } | |
| 632 | + $migrator::addCustomerRecoveryIndex(); | |
| 633 | + if (!$migrator::hasCustomerRecoveryIndex()) { | |
| 634 | + return; // Keep the upgrade pending if ALTER failed. | |
| 635 | + } | |
| 636 | + } | |
| 637 | + update_option($option, '1', false); | |
| 638 | + } finally { | |
| 544 | 639 | self::releaseMigrationLock(); |
| 545 | 640 | } |
| 546 | 641 | } |
| 547 | 642 | |