PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.0
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / database / DBMigrator.php

DBMigrator.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.0, at database/DBMigrator.php

562 lines 29.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Database;
4
5 use FluentCart\App\CPT\FluentProducts;
6 use FluentCart\App\Models\Product;
7
8 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
9
10 use FluentCart\Database\Migrations\AttributeGroupsMigrator;
11 use FluentCart\Database\Migrations\AttributeObjectRelationsMigrator;
12 use FluentCart\Database\Migrations\AttributeTermsMigrator;
13 use FluentCart\Database\Migrations\CartMigrator;
14 use FluentCart\Database\Migrations\CustomersMigrator;
15 use FluentCart\Database\Migrations\MetaMigrator;
16 use FluentCart\Database\Migrations\Migrator;
17 use FluentCart\Database\Migrations\OrderMetaMigrator;
18 use FluentCart\Database\Migrations\OrdersMigrator;
19 use FluentCart\Database\Migrations\OrdersItemsMigrator;
20 use FluentCart\Database\Migrations\OrderTransactionsMigrator;
21 use FluentCart\Database\Migrations\ProductDetailsMigrator;
22 use FluentCart\Database\Migrations\ProductDownloadsMigrator;
23 use FluentCart\Database\Migrations\ProductMetaMigrator;
24 use FluentCart\Database\Migrations\ProductVariationMigrator;
25 use FluentCart\Database\Migrations\ScheduledActionsMigrator;
26 use FluentCart\Database\Migrations\ShippingClassesMigrator;
27 use FluentCart\Database\Migrations\SubscriptionMetaMigrator;
28 use FluentCart\Database\Migrations\SubscriptionsMigrator;
29 use FluentCart\Database\Migrations\TaxClassesMigrator;
30 use FluentCart\Database\Migrations\TaxRatesMigrator;
31 use FluentCart\Database\Migrations\OrderTaxRateMigrator;
32 use FluentCart\Database\Migrations\CouponsMigrator;
33 use FluentCart\Database\Migrations\CustomerAddressesMigrator;
34 use FluentCart\Database\Migrations\CustomerMetaMigrator;
35 use FluentCart\Database\Migrations\OrderAddressesMigrator;
36 use FluentCart\Database\Migrations\OrderDownloadPermissionsMigrator;
37 use FluentCart\Database\Migrations\OrderOperationsMigrator;
38 use FluentCart\Database\Migrations\AppliedCouponsMigrator;
39 use FluentCart\Database\Migrations\LabelMigrator;
40 use FluentCart\Database\Migrations\LabelRelationshipsMigrator;
41 use FluentCart\Database\Migrations\ActivityMigrator;
42 use FluentCart\Database\Migrations\WebhookLogger;
43 use FluentCart\Framework\Database\Schema;
44 use FluentCartPro\App\Modules\Licensing\Models\License;
45 use FluentCartPro\App\Modules\Licensing\Models\LicenseActivation;
46 use FluentCartPro\App\Modules\Licensing\Models\LicenseMeta;
47 use FluentCartPro\App\Modules\Licensing\Models\LicenseSite;
48 use FluentCart\Database\Migrations\ShippingZonesMigrator;
49 use FluentCart\Database\Migrations\ShippingMethodsMigrator;
50 use FluentCart\Database\Migrations\RetentionSnapshotsMigrator;
51
52 class DBMigrator
53 {
54 private static array $migrators = [
55 MetaMigrator::class,
56 AttributeGroupsMigrator::class,
57 AttributeObjectRelationsMigrator::class,
58 AttributeTermsMigrator::class,
59 CartMigrator::class,
60 CouponsMigrator::class,
61 CustomerAddressesMigrator::class,
62 CustomerMetaMigrator::class,
63 CustomersMigrator::class,
64 OrderAddressesMigrator::class,
65 OrderDownloadPermissionsMigrator::class,
66 OrderOperationsMigrator::class,
67 OrdersItemsMigrator::class,
68 OrdersMigrator::class,
69 OrderTaxRateMigrator::class,
70 OrderMetaMigrator::class,
71 OrderTransactionsMigrator::class,
72 ProductDetailsMigrator::class,
73 ProductDownloadsMigrator::class,
74 ProductMetaMigrator::class,
75 SubscriptionMetaMigrator::class,
76 SubscriptionsMigrator::class,
77 TaxClassesMigrator::class,
78 TaxRatesMigrator::class,
79 ProductVariationMigrator::class,
80 AppliedCouponsMigrator::class,
81 LabelMigrator::class,
82 LabelRelationshipsMigrator::class,
83 ActivityMigrator::class,
84 WebhookLogger::class,
85 ShippingZonesMigrator::class,
86 ShippingMethodsMigrator::class,
87 ShippingClassesMigrator::class,
88 ScheduledActionsMigrator::class,
89 RetentionSnapshotsMigrator::class
90 ];
91
92 public static function migrateUp($network_wide = false)
93 {
94 global $wpdb;
95 if ($network_wide) {
96 // Retrieve all site IDs from this network (WordPress >= 4.6 provides easy to use functions for that).
97 if (function_exists('get_sites') && function_exists('get_current_network_id')) {
98 $site_ids = get_sites(array('fields' => 'ids', 'network_id' => get_current_network_id()));
99 } else {
100 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
101 $site_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs WHERE site_id = $wpdb->siteid;");
102 }
103 // Install the plugin for all these sites.
104 foreach ($site_ids as $site_id) {
105 switch_to_blog($site_id);
106 self::run_migrate();
107 restore_current_blog();
108 }
109 } else {
110 self::run_migrate();
111 }
112 }
113
114 public static function run_migrate()
115 {
116 self::migrate();
117 self::maybeMigrateDBChanges();
118 update_option('_fluent_cart_db_version', FLUENTCART_DB_VERSION, 'no');
119 }
120
121 public static function migrate()
122 {
123 /**
124 * @var $migrator Migrator
125 */
126 foreach (self::$migrators as $migrator) {
127 $migrator::migrate();
128 }
129 }
130
131 public static function maybeMigrateDBChanges()
132 {
133 /*
134 * TODO We will remove this after final release
135 */
136 $currentDBVersion = get_option('_fluent_cart_db_version');
137
138 if (!$currentDBVersion || version_compare($currentDBVersion, FLUENTCART_DB_VERSION, '<')) {
139
140 update_option('_fluent_cart_db_version', FLUENTCART_DB_VERSION, 'no');
141
142 // 2026-04-25
143 TaxRatesMigrator::upgradeShippingOverridePrecision();
144
145 // 2026-05-27
146 TaxRatesMigrator::fixPostcodeRangeSeparator();
147 OrdersMigrator::addFeeTotalColumn();
148
149 // 2026-05-10
150 AttributeGroupsMigrator::dropLegacyTitleUniqueIndexes();
151
152 // let's check the orders table sequence number
153 global $wpdb;
154
155 // Product Meta unique index removal
156 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
157 $hasProductMetaUnqIndex = $wpdb->get_col($wpdb->prepare("SELECT * FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND INDEX_NAME='" . $wpdb->prefix . "fct_pm__comp_unq' AND TABLE_NAME=%s", $wpdb->prefix . 'fct_product_meta'));
158 if ($hasProductMetaUnqIndex) {
159
160 $table_name = $wpdb->prefix . 'fct_product_meta';
161 $index_name = 'fct_pm__comp_unq';
162
163 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
164 $wpdb->query(
165 $wpdb->prepare(
166 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
167 "ALTER TABLE %i DROP INDEX %i",
168 $table_name,
169 $index_name
170 ));
171
172 }
173
174 if (!Schema::hasColumn('tax_behavior', 'fct_orders')) {
175 $table_name = $wpdb->prefix . 'fct_orders';
176 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
177 $wpdb->query($wpdb->prepare(
178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
179 "ALTER TABLE %i ADD COLUMN `tax_behavior` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 => no_tax, 1 => exclusive, 2 => inclusive' AFTER `rate`",
180 $table_name
181 ));
182 }
183
184 if (!Schema::hasColumn('slug', 'fct_tax_classes')) {
185 $table_name = $wpdb->prefix . 'fct_tax_classes';
186 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
187 $wpdb->query($wpdb->prepare(
188 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
189 "ALTER TABLE %i ADD COLUMN `slug` VARCHAR(100) NULL AFTER `title`",
190 $table_name
191 ));
192 }
193
194 $ordersTable = $wpdb->prefix . 'fct_orders';
195
196 if (!Schema::hasColumn('fee_total', 'fct_orders')) {
197 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
198 $wpdb->query($wpdb->prepare(
199 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
200 "ALTER TABLE %i ADD COLUMN `fee_total` BIGINT NOT NULL DEFAULT '0' AFTER `shipping_total`",
201 $ordersTable
202 ));
203 }
204
205 // check if scheduled_at is exist or not
206 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
207 $isReceiptNumberMigrated = $wpdb->get_col($wpdb->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND COLUMN_NAME='receipt_number' AND TABLE_NAME=%s", $ordersTable));
208 if (!$isReceiptNumberMigrated) {
209 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
210 $wpdb->query($wpdb->prepare(
211 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
212 "ALTER TABLE %i ADD COLUMN `receipt_number` BIGINT NULL AFTER `parent_id`",
213 $ordersTable
214 ));
215 }
216
217 /**
218 * Changing fct_meta.key to fct_meta.meta_key
219 */
220 if (Schema::hasColumn('discount_total', 'fct_orders')) {
221 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
222 $wpdb->query($wpdb->prepare(
223 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
224 "ALTER TABLE %i CHANGE `discount_total` `manual_discount_total` BIGINT NOT NULL DEFAULT '0'",
225 $ordersTable
226 ));
227 }
228
229 /**
230 * Changing fct_meta.key to fct_meta.meta_key
231 */
232 if (Schema::hasColumn('key', 'fct_meta')) {
233 $table_name = $wpdb->prefix . 'fct_meta';
234 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
235 $wpdb->query($wpdb->prepare(
236 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
237 "ALTER TABLE %i CHANGE `key` `meta_key` VARCHAR(192)",
238 $table_name
239 ));
240 }
241
242 /**
243 * Changing fct_meta.value to fct_meta.meta_value
244 */
245 if (Schema::hasColumn('value', 'fct_meta')) {
246 $table_name = $wpdb->prefix . 'fct_meta';
247 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
248 $wpdb->query($wpdb->prepare(
249 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
250 "ALTER TABLE %i CHANGE `value` `meta_value` LONGTEXT",
251 $table_name
252 ));
253 }
254
255 /**
256 * Changing fct_order_meta.key to fct_order_meta.meta_key and fct_order_meta.value to fct_order_meta.meta_value
257 */
258 if (Schema::hasColumn('key', 'fct_order_meta')) {
259 $table_name = $wpdb->prefix . 'fct_order_meta';
260 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
261 $wpdb->query($wpdb->prepare(
262 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
263 "ALTER TABLE %i CHANGE `key` `meta_key` VARCHAR(192)",
264 $table_name
265 ));
266 }
267 /**
268 * Changing fct_meta.key to fct_meta.meta_key and fct_meta.value to fct_meta.meta_value
269 */
270 if (Schema::hasColumn('value', 'fct_order_meta')) {
271 $table_name = $wpdb->prefix . 'fct_order_meta';
272 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
273 $wpdb->query($wpdb->prepare(
274 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
275 "ALTER TABLE %i CHANGE `value` `meta_value` LONGTEXT",
276 $table_name
277 ));
278 }
279
280 /**
281 * adding ltv column to fct_customers table
282 */
283 if (!Schema::hasColumn('ltv', 'fct_customers')) {
284 $table_name = $wpdb->prefix . 'fct_customers';
285 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
286 $wpdb->query($wpdb->prepare(
287 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
288 "ALTER TABLE %i ADD COLUMN `ltv` BIGINT NOT NULL DEFAULT '0' AFTER `purchase_count`",
289 $table_name
290 ));
291 }
292
293 /**
294 * adding states column to fct_shipping_methods table
295 */
296 if (!Schema::hasColumn('states', 'fct_shipping_methods')) {
297 $table_name = $wpdb->prefix . 'fct_shipping_methods';
298 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
299 $wpdb->query($wpdb->prepare(
300 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
301 "ALTER TABLE %i ADD COLUMN `states` LONGTEXT NULL AFTER `is_enabled`",
302 $table_name
303 ));
304 }
305
306 /**
307 * modify states column to json in fct_shipping_methods table
308 */
309 if (Schema::hasColumn('states', 'fct_shipping_methods')) {
310 $table_name = $wpdb->prefix . 'fct_shipping_methods';
311 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
312 $wpdb->query($wpdb->prepare(
313 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
314 "ALTER TABLE %i MODIFY COLUMN `states` JSON NULL",
315 $table_name
316 ));
317 }
318
319 /**
320 * Changing fct_shipping_zones.regions to fct_shipping_zones.region
321 */
322 if (Schema::hasColumn('regions', 'fct_shipping_zones')) {
323 $table_name = $wpdb->prefix . 'fct_shipping_zones';
324 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
325 $wpdb->query($wpdb->prepare(
326 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
327 "ALTER TABLE %i CHANGE `regions` `region` VARCHAR(192) NOT NULL",
328 $table_name
329 ));
330 }
331
332 /**
333 * adding uuid column to fct_subscriptions table
334 */
335
336 if (!Schema::hasColumn('uuid', 'fct_subscriptions')) {
337 $table_name = $wpdb->prefix . 'fct_subscriptions';
338 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
339 $wpdb->query($wpdb->prepare(
340 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
341 "ALTER TABLE %i ADD COLUMN `uuid` VARCHAR(100) NOT NULL AFTER `id`",
342 $table_name
343 ));
344 }
345
346 if (Schema::hasColumn('initial_amount', 'fct_subscriptions')) {
347 $table_name = $wpdb->prefix . 'fct_subscriptions';
348 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
349 $wpdb->query($wpdb->prepare(
350 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
351 "ALTER TABLE %i CHANGE `initial_amount` `signup_fee` BIGINT UNSIGNED NOT NULL DEFAULT 0",
352 $table_name
353 ));
354 }
355
356 SubscriptionsMigrator::backfillEmptyUuids();
357
358 if (!Schema::hasColumn('meta', 'fct_order_addresses')) {
359 $table_name = $wpdb->prefix . 'fct_order_addresses';
360 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
361 $wpdb->query($wpdb->prepare(
362 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
363 "ALTER TABLE %i ADD COLUMN `meta` JSON DEFAULT NULL AFTER `country`",
364 $table_name
365 ));
366 }
367
368 if (!Schema::hasColumn('meta', 'fct_customer_addresses')) {
369 $table_name = $wpdb->prefix . 'fct_customer_addresses';
370 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
371 $wpdb->query($wpdb->prepare(
372 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
373 "ALTER TABLE %i ADD COLUMN `meta` JSON DEFAULT NULL AFTER `country`",
374 $table_name
375 ));
376 }
377
378 if (!Schema::hasColumn('meta', 'fct_order_tax_rate')) {
379 $table_name = $wpdb->prefix . 'fct_order_tax_rate';
380 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
381 $wpdb->query($wpdb->prepare(
382 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
383 "ALTER TABLE %i ADD COLUMN `meta` JSON",
384 $table_name
385 ));
386 }
387
388 if (!Schema::hasColumn('filed_at', 'fct_order_tax_rate')) {
389 $table_name = $wpdb->prefix . 'fct_order_tax_rate';
390 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
391 $wpdb->query($wpdb->prepare(
392 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
393 "ALTER TABLE %i ADD COLUMN `filed_at` DATETIME NULL AFTER `meta`",
394 $table_name
395 ));
396 }
397
398 if (Schema::hasColumn('categories', 'fct_tax_classes') && !Schema::hasColumn('meta', 'fct_tax_classes')) {
399 $table_name = $wpdb->prefix . 'fct_tax_classes';
400 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
401 $wpdb->query($wpdb->prepare(
402 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
403 "ALTER TABLE %i CHANGE `categories` `meta` JSON",
404 $table_name
405 ));
406 }
407
408 if (!Schema::hasColumn('meta', 'fct_tax_classes')) {
409 $table_name = $wpdb->prefix . 'fct_tax_classes';
410 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
411 $wpdb->query($wpdb->prepare(
412 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
413 "ALTER TABLE %i ADD COLUMN `meta` JSON",
414 $table_name
415 ));
416 }
417
418 if (!Schema::hasColumn('description', 'fct_tax_classes')) {
419 $table_name = $wpdb->prefix . 'fct_tax_classes';
420 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
421 $wpdb->query($wpdb->prepare(
422 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
423 "ALTER TABLE %i ADD COLUMN `description` LONGTEXT NULL AFTER `title`",
424 $table_name
425 ));
426 }
427
428 if (!Schema::hasColumn('group', 'fct_tax_rates')) {
429 $table_name = $wpdb->prefix . 'fct_tax_rates';
430 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
431 $wpdb->query($wpdb->prepare(
432 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
433 "ALTER TABLE %i ADD COLUMN `group` VARCHAR(45) NULL AFTER `name`",
434 $table_name
435 ));
436 }
437
438 if (!Schema::hasColumn('meta', 'fct_shipping_methods')) {
439 $table_name = $wpdb->prefix . 'fct_shipping_methods';
440 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
441 $wpdb->query($wpdb->prepare(
442 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
443 "ALTER TABLE %i ADD COLUMN `meta` JSON NULL AFTER `order`",
444 $table_name
445 ));
446
447 }
448
449 // Shipping schema upgrades — also run here so plugin updates
450 // without deactivation/reactivation still apply new columns
451 \FluentCart\Database\Migrations\ShippingZonesMigrator::migrated();
452 \FluentCart\Database\Migrations\ShippingClassesMigrator::migrated();
453 \FluentCart\Database\Migrations\SubscriptionsMigrator::migrated();
454
455
456 // 2026-05-06
457 // Backfill, dedup, then add unique index — must run in this order
458 // so the constraint can be applied without "Duplicate entry" errors.
459 TaxClassesMigrator::backfillNullSlugs();
460 TaxClassesMigrator::deduplicateSlugs();
461 TaxClassesMigrator::addSlugUniqueIndex();
462
463 // Ensure Standard tax class exists for all installs
464 TaxClassesMigrator::seedDefaultTaxClass();
465
466 // 2026-05-15
467 // Move EU VAT country registrations from wp_options blob to fct_meta rows.
468 \FluentCart\Database\Migrations\EuVatRegistrationMigrator::migrate();
469
470 // 2026-06-10
471 // One-time migration: move legacy price_suffix into the correct split field
472 // based on the store's tax_inclusion setting at the time of migration.
473 if (!get_option('_fluent_cart_price_suffix_migrated')) {
474 $taxSettings = get_option('fluent_cart_tax_configuration_settings', []);
475 $legacySuffix = isset($taxSettings['price_suffix']) ? $taxSettings['price_suffix'] : '';
476 $hasIncluded = isset($taxSettings['price_suffix_included']) && $taxSettings['price_suffix_included'] !== '';
477 $hasExcluded = isset($taxSettings['price_suffix_excluded']) && $taxSettings['price_suffix_excluded'] !== '';
478
479 if ($legacySuffix !== '' && !$hasIncluded && !$hasExcluded) {
480 $taxInclusion = isset($taxSettings['tax_inclusion']) ? $taxSettings['tax_inclusion'] : 'excluded';
481 if ($taxInclusion === 'excluded') {
482 $taxSettings['price_suffix_excluded'] = $legacySuffix;
483 } else {
484 $taxSettings['price_suffix_included'] = $legacySuffix;
485 }
486 update_option('fluent_cart_tax_configuration_settings', $taxSettings, true);
487 }
488 update_option('_fluent_cart_price_suffix_migrated', '1', 'no');
489 }
490
491 // 2026-05-18
492 // One-time migration: copy seller identity fields from the Pro seller_details fct_meta
493 // entry into fluent_cart_store_settings. Guarded by a sentinel so it runs exactly once.
494 if (!get_option('_fluent_cart_seller_details_migrated')) {
495 $sellerDetails = fluent_cart_get_option('seller_details', []);
496 if (is_array($sellerDetails) && !empty($sellerDetails)) {
497 $storeSettingsOption = get_option('fluent_cart_store_settings', []);
498 if (!is_array($storeSettingsOption)) {
499 $storeSettingsOption = [];
500 }
501 $fieldMap = [
502 'seller_vat_id' => 'seller_vat_id',
503 'seller_tax_id' => 'seller_tax_id',
504 'seller_legal_name' => 'company_name',
505 'seller_legal_registration_id' => 'legal_registration_id',
506 ];
507 $changed = false;
508 foreach ($fieldMap as $fromKey => $toKey) {
509 if (!empty($sellerDetails[$fromKey]) && empty($storeSettingsOption[$toKey])) {
510 $storeSettingsOption[$toKey] = sanitize_text_field($sellerDetails[$fromKey]);
511 $changed = true;
512 }
513 }
514 if ($changed) {
515 update_option('fluent_cart_store_settings', $storeSettingsOption, true);
516 }
517 }
518 update_option('_fluent_cart_seller_details_migrated', '1', 'no');
519 }
520
521 // 2026-06-03
522 // Fix: convert zero-date datetime columns to NULL (Issue #1911).
523 // Zero-dates arise on MySQL servers without STRICT_TRANS_TABLES when an
524 // empty string is written to a DATETIME column.
525 $table_name = $wpdb->prefix . 'fct_subscriptions';
526 foreach (['next_billing_date', 'canceled_at', 'expire_at'] as $col) {
527 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
528 $wpdb->query($wpdb->prepare(
529 "UPDATE %i SET `{$col}` = NULL WHERE `{$col}` IN ('0000-00-00 00:00:00', '0000-00-00')",
530 $table_name
531 ));
532 }
533 }
534 }
535
536 public static function migrateDown($network_wide = false)
537 {
538 /**
539 * @var $migrator Migrator
540 */
541 foreach (self::$migrators as $migrator) {
542 $migrator::dropTable();
543 }
544
545 Product::query()->where('post_type', '=', FluentProducts::CPT_NAME)->delete();
546
547 //Migrate Down The Licenses
548 if (class_exists(License::class)) {
549 License::query()->truncate();
550 LicenseActivation::query()->truncate();
551 LicenseSite::query()->truncate();
552 LicenseMeta::query()->truncate();
553 }
554 }
555
556 public static function refresh($network_wide = false)
557 {
558 static::migrateDown($network_wide);
559 static::migrateUp($network_wide);
560 }
561 }
562