PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 trunk All 48 releases
← All changes | database/Migrations/ProductVariationMigrator.php +183 -0 1.3.26 → 1.6.5 View file →
@@ -3,8 +3,11 @@
3 3 namespace FluentCart\Database\Migrations;
4 4
5 5 class ProductVariationMigrator extends Migrator
6 6 {
7 + protected static int $chunkSize = 500;
8 + protected static string $taxBackfillCompletionOption = '_fluent_cart_variation_tax_backfill_completed';
9 +
7 10 public static string $tableName = 'fct_product_variations';
8 11
9 12 public static function getSqlSchema(): string
10 13 {
@@ -43,8 +46,9 @@
43 46
44 47 public static function migrated()
45 48 {
46 49 static::addSkuColumn();
50 + static::backfillProductLevelTaxToVariations();
47 51 }
48 52
49 53 public static function addSkuColumn()
50 54 {
@@ -51,6 +55,185 @@
51 55 // "ALTER TABLE %i ADD COLUMN `sku` VARCHAR(30) NULL DEFAULT NULL AFTER `variation_identifier`"
52 56 static::addColumnIfNotExists('sku', 'VARCHAR(30) NULL DEFAULT NULL', 'variation_identifier');
53 57 // "ALTER TABLE %i ADD UNIQUE INDEX `sku_unique` (`sku` ASC)"
54 58 static::addIndexIfNotExists('sku_unique', 'sku', true);
59 + }
60 +
61 + /**
62 + * Copy tax settings from product.detail.other_info down to each variation that has
63 + * no explicit override. Runs only on variations that are missing the key entirely —
64 + * any variation that already carries its own tax_exempt or tax_class is left untouched.
65 + *
66 + * Idempotent: updates only missing variation keys, and marks completion so future
67 + * migration runs do not rescan every product detail row on activation.
68 + */
69 + public static function backfillProductLevelTaxToVariations()
70 + {
71 + if (get_option(static::$taxBackfillCompletionOption) === 'yes') {
72 + return;
73 + }
74 +
75 + global $wpdb;
76 +
77 + $detailsTable = $wpdb->prefix . 'fct_product_details';
78 + $variationsTable = $wpdb->prefix . 'fct_product_variations';
79 + $lastId = 0;
80 + $taxClassSlugMap = [];
81 +
82 + do {
83 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
84 + $rows = $wpdb->get_results($wpdb->prepare(
85 + "SELECT `id`, `post_id`, `other_info`
86 + FROM `{$detailsTable}`
87 + WHERE `id` > %d
88 + ORDER BY `id` ASC
89 + LIMIT %d",
90 + $lastId,
91 + static::$chunkSize
92 + ));
93 +
94 + if (empty($rows)) {
95 + break;
96 + }
97 +
98 + $qualifyingRows = [];
99 + $postIds = [];
100 +
101 + foreach ($rows as $row) {
102 + $lastId = (int) $row->id;
103 +
104 + $detailInfo = json_decode($row->other_info, true);
105 + if (!is_array($detailInfo)) {
106 + continue;
107 + }
108 +
109 + $productTaxExempt = isset($detailInfo['tax_exempt']) ? (string) $detailInfo['tax_exempt'] : '';
110 + $productTaxClass = isset($detailInfo['tax_class']) ? (string) $detailInfo['tax_class'] : '';
111 +
112 + // Resolve tax_class stored as a numeric ID (product level) to its slug (variation level).
113 + $taxClassSlug = static::resolveVariationTaxClassSlug($productTaxClass, $taxClassSlugMap);
114 +
115 + $needsExempt = ($productTaxExempt === 'yes');
116 + $needsClass = ($taxClassSlug !== '');
117 +
118 + if (!$needsExempt && !$needsClass) {
119 + continue;
120 + }
121 +
122 + $postId = (int) $row->post_id;
123 + $qualifyingRows[] = [
124 + 'post_id' => $postId,
125 + 'needs_exempt' => $needsExempt,
126 + 'tax_class' => $taxClassSlug,
127 + ];
128 + $postIds[$postId] = $postId;
129 + }
130 +
131 + if (empty($qualifyingRows)) {
132 + continue;
133 + }
134 +
135 + $variationGroups = [];
136 + $variationInfoMap = [];
137 + $placeholders = implode(', ', array_fill(0, count($postIds), '%d'));
138 +
139 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
140 + $variations = $wpdb->get_results($wpdb->prepare(
141 + "SELECT `id`, `post_id`, `other_info`
142 + FROM `{$variationsTable}`
143 + WHERE `post_id` IN ({$placeholders})",
144 + array_values($postIds)
145 + ));
146 +
147 + foreach ($variations as $variation) {
148 + $variationGroups[(int) $variation->post_id][] = $variation;
149 + }
150 +
151 + foreach ($qualifyingRows as $qualifyingRow) {
152 + $variations = $variationGroups[$qualifyingRow['post_id']] ?? [];
153 +
154 + foreach ($variations as $variation) {
155 + $variationId = (int) $variation->id;
156 +
157 + if (!array_key_exists($variationId, $variationInfoMap)) {
158 + $varInfo = !empty($variation->other_info)
159 + ? json_decode($variation->other_info, true)
160 + : [];
161 +
162 + if (!is_array($varInfo)) {
163 + $varInfo = [];
164 + }
165 +
166 + $variationInfoMap[$variationId] = $varInfo;
167 + }
168 +
169 + $varInfo = $variationInfoMap[$variationId];
170 + $updated = false;
171 +
172 + // Only set tax_exempt when the variation has no explicit value at all.
173 + if ($qualifyingRow['needs_exempt'] && !array_key_exists('tax_exempt', $varInfo)) {
174 + $varInfo['tax_exempt'] = 'yes';
175 + $updated = true;
176 + }
177 +
178 + // Only set tax_class when the variation has no explicit value at all.
179 + if ($qualifyingRow['tax_class'] !== '' && !array_key_exists('tax_class', $varInfo)) {
180 + $varInfo['tax_class'] = $qualifyingRow['tax_class'];
181 + $updated = true;
182 + }
183 +
184 + if (!$updated) {
185 + continue;
186 + }
187 +
188 + $variationInfoMap[$variationId] = $varInfo;
189 +
190 + $wpdb->update(
191 + $variationsTable,
192 + ['other_info' => wp_json_encode($varInfo)],
193 + ['id' => $variationId],
194 + ['%s'],
195 + ['%d']
196 + );
197 + }
198 + }
199 + } while (count($rows) === static::$chunkSize);
200 +
201 + update_option(static::$taxBackfillCompletionOption, 'yes', 'no');
202 + }
203 +
204 + protected static function resolveVariationTaxClassSlug($productTaxClass, array &$taxClassSlugMap): string
205 + {
206 + if ($productTaxClass === '') {
207 + return '';
208 + }
209 +
210 + if (is_numeric($productTaxClass)) {
211 + $taxClassId = (int) $productTaxClass;
212 +
213 + if (!array_key_exists($taxClassId, $taxClassSlugMap)) {
214 + global $wpdb;
215 +
216 + $taxClassTable = $wpdb->prefix . 'fct_tax_classes';
217 +
218 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
219 + $slug = $wpdb->get_var($wpdb->prepare(
220 + "SELECT `slug` FROM `{$taxClassTable}` WHERE `id` = %d LIMIT 1",
221 + $taxClassId
222 + ));
223 +
224 + $taxClassSlugMap[$taxClassId] = $slug ? sanitize_key((string) $slug) : '';
225 + }
226 +
227 + $productTaxClass = $taxClassSlugMap[$taxClassId];
228 + } else {
229 + $productTaxClass = sanitize_key($productTaxClass);
230 + }
231 +
232 + // 'standard' is already the variation default — nothing to backfill.
233 + if ($productTaxClass === 'standard') {
234 + return '';
235 + }
236 +
237 + return $productTaxClass;
55 238 }
56 239 }