PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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 / app / Modules / WooCommerceMigrator / WooCommerceMigratorCli.php

WooCommerceMigratorCli.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.1, at app/Modules/WooCommerceMigrator/WooCommerceMigratorCli.php

1,079 lines 44.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\WooCommerceMigrator;
4
5 use FluentCart\Framework\Support\Arr;
6 use FluentCart\App\CPT\FluentProducts;
7 use WP_CLI;
8
9 /**
10 * WooCommerceMigratorCli
11 *
12 * This class handles the migration of WooCommerce products, categories, attachments, and downloadable files to FluentCart.
13 * It provides CLI commands for bulk migration and ensures data is mapped and transformed to match FluentCart's structure and logic.
14 *
15 * Major responsibilities:
16 * - Migrate product posts, variations, categories, and attachments
17 * - Map WooCommerce product types, stock, downloadable, and virtual flags to FluentCart equivalents
18 * - Copy downloadable files to FluentCart's upload directory
19 * - Ensure all product meta, images, and downloadable assets are correctly linked
20 */
21 class WooCommerceMigratorCli
22 {
23 private $attachmentMap = [];
24 private $migrationSteps = [];
25 private $categoryMap = [];
26
27 public function __construct()
28 {
29 $this->migrationSteps = get_option('__fluent_cart_wc_migration_steps', [
30 'attachments' => 'no',
31 'products' => 'no',
32 'variations' => 'no',
33 'categories' => 'no'
34 ]);
35
36 $this->categoryMap = get_option('__fluent_cart_wc_category_map', []);
37 }
38
39 private function checkWooCommerceDependencies()
40 {
41 if (!class_exists('WooCommerce')) {
42 return new \WP_Error('wc_migrator_error', __('WooCommerce is not installed or activated.', 'fluent-cart'));
43 }
44 return true;
45 }
46
47 /**
48 * Migrate all WooCommerce attachments (media files) to FluentCart.
49 *
50 * This method finds all WooCommerce attachments and copies them to the FluentCart media library if needed.
51 * It also copies attachment meta and ensures images are available for migrated products and variations.
52 */
53 public function migrateAttachments()
54 {
55 if ($this->migrationSteps['attachments'] == 'yes') {
56 $this->attachmentMap = get_option('__fluent_cart_wc_attachment_map', []);
57 return $this->attachmentMap;
58 }
59
60 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
61 $attachments = get_posts([
62 'post_type' => 'attachment',
63 'post_status' => 'any',
64 'posts_per_page' => -1,
65 'fields' => 'all',
66 ]);
67
68 foreach ($attachments as $attachment) {
69 $newAttachmentId = $this->migrateSingleAttachment($attachment);
70 if (!is_wp_error($newAttachmentId)) {
71 $this->attachmentMap[$attachment->ID] = $newAttachmentId;
72 }
73 }
74
75 update_option('__fluent_cart_wc_attachment_map', $this->attachmentMap);
76 $this->migrationSteps['attachments'] = 'yes';
77 update_option('__fluent_cart_wc_migration_steps', $this->migrationSteps);
78
79 return $this->attachmentMap;
80 }
81
82 private function migrateSingleAttachment($attachment)
83 {
84 // Check if attachment already exists by GUID
85 $existingAttachment = get_posts([
86 'post_type' => 'attachment',
87 'guid' => $attachment->guid,
88 'posts_per_page' => 1
89 ]);
90
91 if ($existingAttachment) {
92 return $existingAttachment[0]->ID;
93 }
94
95 // Get attachment file path and URL
96 $uploadDir = wp_upload_dir();
97 $filePath = get_attached_file($attachment->ID);
98 $fileUrl = wp_get_attachment_url($attachment->ID);
99
100 if (!$filePath || !file_exists($filePath)) {
101 return new \WP_Error('attachment_not_found', 'Attachment file not found: ' . $filePath);
102 }
103
104 // Copy file to new location if needed
105 $fileName = basename($filePath);
106 $newFilePath = $uploadDir['path'] . '/' . $fileName;
107
108 if ($filePath !== $newFilePath && !file_exists($newFilePath)) {
109 copy($filePath, $newFilePath);
110 }
111
112 // Prepare attachment data
113 $attachmentData = [
114 'post_mime_type' => $attachment->post_mime_type,
115 'post_title' => $attachment->post_title,
116 'post_content' => $attachment->post_content,
117 'post_excerpt' => $attachment->post_excerpt,
118 'post_status' => 'inherit',
119 'guid' => $fileUrl
120 ];
121
122 // Insert attachment
123 $newAttachmentId = wp_insert_attachment($attachmentData, $newFilePath);
124 if (is_wp_error($newAttachmentId)) {
125 return $newAttachmentId;
126 }
127
128 // Generate attachment metadata
129 require_once(ABSPATH . 'wp-admin/includes/image.php');
130 require_once(ABSPATH . 'wp-admin/includes/media.php');
131 $attachmentData = wp_generate_attachment_metadata($newAttachmentId, $newFilePath);
132 wp_update_attachment_metadata($newAttachmentId, $attachmentData);
133
134 // Copy attachment meta
135 $meta = get_post_meta($attachment->ID);
136 if ($meta) {
137 foreach ($meta as $key => $values) {
138 foreach ($values as $value) {
139 update_post_meta($newAttachmentId, $key, maybe_unserialize($value));
140 }
141 }
142 }
143
144 return $newAttachmentId;
145 }
146
147 /**
148 * Migrate all WooCommerce products to FluentCart.
149 *
150 * This is the main entry point for product migration. It migrates categories first, then all products.
151 * For each product, it handles variations, images, downloadable files, stock, and meta mapping.
152 *
153 * @param bool $willUpdate Whether to update existing FluentCart products
154 * @return array|\WP_Error Migration results or error
155 */
156 public function migrate_products($willUpdate = false)
157 {
158 $check = $this->checkWooCommerceDependencies();
159 if (is_wp_error($check)) {
160 return $check;
161 }
162
163 // Migrate categories and brands first
164 $this->migrateCategories();
165 $this->migrateBrands();
166
167 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
168 $wooProducts = get_posts([
169 'post_type' => 'product',
170 'posts_per_page' => -1,
171 'fields' => 'all',
172 'post_status' => ['publish', 'draft', 'pending', 'private'],
173 'no_found_rows' => true,
174 ]);
175
176 if (empty($wooProducts)) {
177 return new \WP_Error('no_products', 'No WooCommerce products found to migrate.');
178 }
179
180 $results = [
181 'success' => 0,
182 'failed' => 0,
183 'failed_ids' => []
184 ];
185
186 foreach ($wooProducts as $wooProduct) {
187 $result = $this->migrateProduct($wooProduct, $willUpdate);
188 if (is_wp_error($result)) {
189 $results['failed']++;
190 $results['failed_ids'][$wooProduct->ID] = $result->get_error_message();
191 } else {
192 $results['success']++;
193 }
194 }
195
196 return $results;
197 }
198
199 /**
200 * Migrate all WooCommerce product categories to FluentCart.
201 *
202 * Ensures all categories and their hierarchy are recreated in FluentCart, including meta and thumbnails.
203 * Maintains a mapping between WooCommerce and FluentCart category IDs for later use.
204 *
205 * Fix: Two-pass migration to ensure parent-child relationships are set correctly.
206 */
207 private function migrateCategories()
208 {
209 // Ensure the product-categories taxonomy is registered
210 if (!taxonomy_exists('product-categories')) {
211 $fluentProducts = new \FluentCart\App\CPT\FluentProducts();
212 $fluentProducts->registerProductTaxonomies();
213 }
214
215 $wooCategories = get_terms([
216 'taxonomy' => 'product_cat',
217 'hide_empty' => false
218 ]);
219
220 if (is_wp_error($wooCategories)) {
221 return $wooCategories;
222 }
223
224 // If categories step is marked as completed, verify that all mapped categories still exist
225 if ($this->migrationSteps['categories'] == 'yes' && !empty($this->categoryMap)) {
226 if ($this->verifyCategoryMappingIntegrity()) {
227 return $this->categoryMap;
228 } else {
229 $this->categoryMap = []; // Reset mapping to force recreation
230 }
231 }
232
233 // First pass: create all categories without parents
234 foreach ($wooCategories as $wooCat) {
235 $newCatId = null;
236 $existingCat = get_term_by('slug', $wooCat->slug, 'product-categories');
237 if ($existingCat) {
238 $newCatId = $existingCat->term_id;
239 } else {
240 $result = wp_insert_term(
241 $wooCat->name,
242 'product-categories',
243 [
244 'description' => $wooCat->description,
245 'slug' => $wooCat->slug,
246 'parent' => 0
247 ]
248 );
249 if (!is_wp_error($result)) {
250 $newCatId = $result['term_id'];
251 }
252 }
253 if ($newCatId) {
254 $this->categoryMap[$wooCat->term_id] = $newCatId;
255 }
256 }
257
258 // Second pass: update parents
259 foreach ($wooCategories as $wooCat) {
260 if ($wooCat->parent && isset($this->categoryMap[$wooCat->parent]) && isset($this->categoryMap[$wooCat->term_id])) {
261 wp_update_term($this->categoryMap[$wooCat->term_id], 'product-categories', ['parent' => $this->categoryMap[$wooCat->parent]]);
262 }
263 }
264
265 update_option('__fluent_cart_wc_category_map', $this->categoryMap);
266 $this->migrationSteps['categories'] = 'yes';
267 update_option('__fluent_cart_wc_migration_steps', $this->migrationSteps);
268
269 return $this->categoryMap;
270 }
271
272 /**
273 * Verify that all mapped categories still exist in the database
274 * @return bool True if all categories exist, false if any are missing
275 */
276 private function verifyCategoryMappingIntegrity()
277 {
278 if (empty($this->categoryMap)) {
279 return false;
280 }
281
282 foreach ($this->categoryMap as $wooCatId => $fluentCatId) {
283 $fluentCat = get_term($fluentCatId, 'product-categories');
284 if (!$fluentCat || is_wp_error($fluentCat)) {
285 return false;
286 }
287 }
288
289 return true;
290 }
291
292 /**
293 * Migrate all WooCommerce brands to FluentCart brands taxonomy.
294 *
295 * Ensures all brands are created and mapped, and mapping is available for product assignment.
296 */
297 private function migrateBrands()
298 {
299 return $this->migrateTaxonomy('product_brand', 'product-brands', '__fluent_cart_wc_brand_map');
300 }
301
302 /**
303 * Migrate a single WooCommerce product (and its variations, images, downloads) to FluentCart.
304 *
305 * Handles mapping of product type, fulfillment type, stock, downloadable/virtual flags, images, gallery, and meta.
306 * For variable products, processes each variation and ensures correct mapping of downloadable files and stock.
307 *
308 * @param object $wooProduct The WooCommerce product post object
309 * @param bool $willUpdate Whether to update existing FluentCart product
310 * @return int|\WP_Error The new FluentCart product ID or error
311 */
312 private function migrateProduct($wooProduct, $willUpdate = false)
313 {
314 try {
315 $productMeta = get_post_meta($wooProduct->ID);
316 $productType = get_post_meta($wooProduct->ID, '_product_type', true);
317 $productType = $productType ?: 'simple';
318
319 // Check for child variations regardless of _product_type meta
320 global $wpdb;
321 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
322 $variationCount = $wpdb->get_var($wpdb->prepare(
323 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_parent = %d AND post_type = 'product_variation'",
324 $wooProduct->ID
325 ));
326 if ($variationCount > 0) {
327 $productType = 'variable';
328 }
329
330 // Clean up price values first
331 $price = floatval(Arr::get($productMeta, '_price', [0])[0]);
332 $regularPrice = floatval(Arr::get($productMeta, '_regular_price', [$price])[0]);
333 $salePrice = floatval(Arr::get($productMeta, '_sale_price', [0])[0]);
334 $mainPrice = $salePrice > 0 ? $salePrice : $price;
335 $minPriceCents = $this->convertToCents($mainPrice);
336 $maxPriceCents = $this->convertToCents($mainPrice);
337 $comparePriceCents = $this->convertToCents($regularPrice);
338
339 $data = [
340 'post_title' => $wooProduct->post_title,
341 'post_content' => $wooProduct->post_content,
342 'post_excerpt' => $wooProduct->post_excerpt,
343 'post_status' => $wooProduct->post_status,
344 'post_type' => 'fluent-products',
345 'post_name' => $wooProduct->post_name,
346 'post_parent' => $wooProduct->post_parent,
347 'menu_order' => $wooProduct->menu_order,
348 'post_date' => $wooProduct->post_date,
349 'post_date_gmt' => $wooProduct->post_date_gmt,
350 'post_modified' => $wooProduct->post_modified,
351 'post_modified_gmt' => $wooProduct->post_modified_gmt
352 ];
353
354 $existingProduct = get_posts([
355 'post_type' => 'fluent-products',
356 'name' => $wooProduct->post_name,
357 'post_status' => 'any',
358 'numberposts' => 1
359 ]);
360
361 $createdPostId = 0;
362 if ($existingProduct && $willUpdate) {
363 $data['ID'] = $existingProduct[0]->ID;
364 $createdPostId = wp_update_post($data);
365 } elseif (!$existingProduct) {
366 $createdPostId = wp_insert_post($data);
367 } else {
368 return new \WP_Error('product_exists', 'Product already exists with slug: ' . $wooProduct->post_name);
369 }
370
371 if (is_wp_error($createdPostId)) {
372 return $createdPostId;
373 }
374
375 // Handle featured image - use existing image ID
376 $thumbnailId = get_post_thumbnail_id($wooProduct->ID);
377 if ($thumbnailId) {
378 set_post_thumbnail($createdPostId, $thumbnailId);
379 }
380
381 // Prepare gallery array for FluentCart - include featured image FIRST, then gallery images
382 $galleryArr = [];
383
384 // Add featured image first (will be the main product image)
385 if ($thumbnailId) {
386 $imgUrl = wp_get_attachment_url($thumbnailId);
387 $imgTitle = get_the_title($thumbnailId);
388 if ($imgUrl) {
389 $galleryArr[] = [
390 'id' => (int)$thumbnailId,
391 'url' => $imgUrl,
392 'title' => $imgTitle
393 ];
394 }
395 }
396
397 // Add gallery images (additional images)
398 $galleryIds = get_post_meta($wooProduct->ID, '_product_image_gallery', true);
399 $galleryIdArr = $galleryIds ? array_filter(array_map('trim', explode(',', $galleryIds))) : [];
400 foreach ($galleryIdArr as $galleryId) {
401 // Skip if this gallery image is the same as featured image
402 if ($galleryId == $thumbnailId) {
403 continue;
404 }
405 $imgUrl = wp_get_attachment_url($galleryId);
406 $imgTitle = get_the_title($galleryId);
407 if ($imgUrl) {
408 $galleryArr[] = [
409 'id' => (int)$galleryId,
410 'url' => $imgUrl,
411 'title' => $imgTitle
412 ];
413 }
414 }
415 if ($galleryArr) {
416 update_post_meta($createdPostId, 'fluent-products-gallery-image', $galleryArr);
417 }
418 if ($galleryIds) {
419 update_post_meta($createdPostId, '_product_image_gallery', $galleryIds);
420 }
421
422 // Handle categories
423 $productCategories = wp_get_post_terms($wooProduct->ID, 'product_cat');
424 if (!is_wp_error($productCategories)) {
425 $newCatIds = [];
426 $catMap = get_option('__fluent_cart_wc_category_map', []);
427 foreach ($productCategories as $cat) {
428 if (isset($catMap[$cat->term_id])) {
429 $newCatIds[] = intval($catMap[$cat->term_id]);
430 }
431 }
432 if ($newCatIds) {
433 wp_set_object_terms($createdPostId, $newCatIds, 'product-categories');
434 }
435 }
436
437 // Brands
438 $productBrands = wp_get_post_terms($wooProduct->ID, 'product_brand');
439 if (!is_wp_error($productBrands)) {
440 $newBrandIds = [];
441 $brandMap = get_option('__fluent_cart_wc_brand_map', []);
442 foreach ($productBrands as $brand) {
443 if (isset($brandMap[$brand->term_id])) {
444 $newBrandIds[] = intval($brandMap[$brand->term_id]);
445 }
446 }
447 if ($newBrandIds) {
448 wp_set_object_terms($createdPostId, $newBrandIds, 'product-brands');
449 }
450 }
451
452 $fulfillmentType = 'physical';
453 if (Arr::get($productMeta, '_downloadable', ['no'])[0] === 'yes') {
454 $fulfillmentType = 'digital';
455 } elseif (Arr::get($productMeta, '_virtual', ['no'])[0] === 'yes') {
456 $fulfillmentType = 'service';
457 }
458
459 // --- Variable Product Logic ---
460 if ($productType === 'variable') {
461 global $wpdb;
462 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
463 $variations = $wpdb->get_results($wpdb->prepare(
464 "SELECT * FROM {$wpdb->posts} WHERE post_parent = %d AND post_type = 'product_variation'",
465 $wooProduct->ID
466 ));
467 if (!empty($variations)) {
468 $variationPrices = [];
469 $variationIds = [];
470 $firstVariationId = null;
471 $variationDownloadMap = []; // Collect downloadable files for all variations
472 $hasDownloadableVariations = false; // Track if any variations have downloads
473 $hasStockManagement = false; // Track if any variation has stock management enabled
474
475 // First pass: Check if any variations have downloadable files and stock management
476 foreach ($variations as $variation) {
477 $variationDownloadableFiles = get_post_meta($variation->ID, '_downloadable_files', true);
478 if (!empty($variationDownloadableFiles)) {
479 $hasDownloadableVariations = true;
480 }
481 $variationMeta = get_post_meta($variation->ID);
482 if (Arr::get($variationMeta, '_manage_stock', ['no'])[0] === 'yes') {
483 $hasStockManagement = true;
484 }
485 if ($hasDownloadableVariations && $hasStockManagement) {
486 break;
487 }
488 }
489
490 // Override fulfillment type if variations have downloads
491 if ($hasDownloadableVariations) {
492 $fulfillmentType = 'digital';
493 }
494
495 foreach ($variations as $variation) {
496 $variationMeta = get_post_meta($variation->ID);
497 $variationSalePrice = floatval(Arr::get($variationMeta, '_sale_price', [0])[0]);
498 $variationPrice = floatval(Arr::get($variationMeta, '_price', [0])[0]);
499 $variationRegularPrice = floatval(Arr::get($variationMeta, '_regular_price', [$variationPrice])[0]);
500 $mainVariationPrice = $variationSalePrice > 0 ? $variationSalePrice : $variationPrice;
501 $variationPrices[] = $mainVariationPrice;
502 $attributes = [];
503 foreach ($variationMeta as $key => $value) {
504 if (strpos($key, 'attribute_') === 0) {
505 $attributeName = str_replace('attribute_', '', $key);
506 $attributes[$attributeName] = $value[0];
507 }
508 }
509 $variationTitle = [];
510 foreach ($attributes as $name => $value) {
511 $term = get_term_by('slug', $value, $name);
512 $variationTitle[] = $term ? $term->name : $value;
513 }
514 $manageStock = $hasStockManagement ? true : (Arr::get($variationMeta, '_manage_stock', ['no'])[0] === 'yes');
515 $stockStatus = get_post_meta($wooProduct->ID, '_stock_status', true) ?: 'instock';
516 if ($stockStatus === 'instock') {
517 $stockStatus = 'in-stock';
518 }
519 $stockQuantity = (int) Arr::get($variationMeta, '_stock', [0])[0];
520 $backorders = Arr::get($variationMeta, '_backorders', ['no'])[0] === 'yes' ? 1 : 0;
521 // Variation-specific image
522 $variationImageId = isset($variationMeta['_thumbnail_id'][0]) ? (int)$variationMeta['_thumbnail_id'][0] : null;
523 // Variation-specific downloads
524 $variationDownloadableFiles = get_post_meta($variation->ID, '_downloadable_files', true);
525 // Set fulfillment_type based on variation's _virtual property
526 $variationFulfillmentType = 'physical';
527 if (Arr::get($variationMeta, '_virtual', ['no'])[0] === 'yes') {
528 $variationFulfillmentType = 'digital';
529 }
530 // Set stock_status to 'out-of-stock' if stock is zero
531 $variationStockStatus = $stockStatus;
532 if ($stockQuantity === 0) {
533 $variationStockStatus = 'out-of-stock';
534 }
535 $variationId = $this->createOrUpdateProductVariations($createdPostId, [
536 'media_id' => $variationImageId,
537 'variation_title' => implode(' - ', $variationTitle) ?: $wooProduct->post_title,
538 'variation_identifier' => $variation->ID,
539 'payment_type' => 'onetime',
540 'fulfillment_type' => $variationFulfillmentType,
541 'item_status' => 'active',
542 'item_price' => $this->convertToCents($mainVariationPrice),
543 'compare_price' => $this->convertToCents($variationRegularPrice),
544 'downloadable' => !empty($variationDownloadableFiles) ? 1 : 0,
545 'manage_stock' => $manageStock ? 1 : 0,
546 'stock_status' => $variationStockStatus,
547 'total_stock' => $stockQuantity,
548 'available' => $stockQuantity,
549 'backorders' => $backorders,
550 'created_at' => current_time('mysql'),
551 'updated_at' => current_time('mysql'),
552 'other_info' => json_encode([
553 'description' => '',
554 'payment_type' => 'onetime',
555 'attributes' => $attributes,
556 'variation_image_id' => $variationImageId
557 ])
558 ]);
559 if (!$firstVariationId) {
560 $firstVariationId = $variationId;
561 }
562 $variationIds[] = $variationId;
563 // Collect downloadable files for variations
564 if (!empty($variationDownloadableFiles)) {
565 $variationDownloadMap[$variationId] = $variationDownloadableFiles;
566 }
567 }
568 // Set product details for variable product
569 $detail = [
570 'post_id' => $createdPostId,
571 'fulfillment_type' => $fulfillmentType,
572 'variation_type' => 'simple_variations',
573 'min_price' => $this->convertToCents(min($variationPrices)),
574 'max_price' => $this->convertToCents(max($variationPrices)),
575 'created_at' => current_time('mysql'),
576 'updated_at' => current_time('mysql'),
577 'manage_stock' => $hasStockManagement ? 1 : 0,
578 'manage_downloadable' => ($fulfillmentType === 'digital') ? 1 : 0,
579 'stock_availability' => 'in-stock',
580 'other_info' => json_encode([
581 'group_pricing_by' => 'payment_type',
582 'use_pricing_table' => 'no'
583 ]),
584 'default_variation_id' => $firstVariationId,
585 'default_media' => null
586 ];
587 $this->updateProductDetails($createdPostId, $detail);
588
589 // Handle downloadable files for all variations
590 if (!empty($variationDownloadMap)) {
591 $this->migrateDownloadableFiles($createdPostId, $variationDownloadMap);
592 }
593 }
594 } else {
595 // --- Simple Product Logic ---
596 $manageStock = get_post_meta($wooProduct->ID, '_manage_stock', true) === 'yes';
597 $stockStatus = get_post_meta($wooProduct->ID, '_stock_status', true) ?: 'instock';
598 if ($stockStatus === 'instock') {
599 $stockStatus = 'in-stock';
600 }
601 $stockQuantity = (int) get_post_meta($wooProduct->ID, '_stock', true);
602 $backorders = get_post_meta($wooProduct->ID, '_backorders', true) === 'yes' ? 1 : 0;
603 $variationId = $this->createOrUpdateProductVariations($createdPostId, [
604 'variation_title' => $wooProduct->post_title,
605 'variation_identifier' => '0',
606 'payment_type' => 'onetime',
607 'fulfillment_type' => $fulfillmentType,
608 'item_status' => 'active',
609 'item_price' => $minPriceCents,
610 'compare_price' => $comparePriceCents,
611 'downloadable' => $fulfillmentType === 'digital' ? 1 : 0,
612 'manage_stock' => $manageStock ? 1 : 0,
613 'stock_status' => $stockStatus,
614 'total_stock' => $stockQuantity,
615 'available' => $stockQuantity,
616 'backorders' => $backorders,
617 'created_at' => current_time('mysql'),
618 'updated_at' => current_time('mysql'),
619 'other_info' => json_encode([
620 'description' => '',
621 'payment_type' => 'onetime'
622 ])
623 ]);
624 $detail = [
625 'post_id' => $createdPostId,
626 'fulfillment_type' => $fulfillmentType,
627 'variation_type' => 'simple',
628 'min_price' => $minPriceCents,
629 'max_price' => $maxPriceCents,
630 'created_at' => current_time('mysql'),
631 'updated_at' => current_time('mysql'),
632 'manage_stock' => $manageStock ? 1 : 0,
633 'manage_downloadable' => ($fulfillmentType === 'digital') ? 1 : 0,
634 'stock_availability' => $stockStatus === 'instock' ? 'in-stock' : 'out-of-stock',
635 'other_info' => json_encode([
636 'group_pricing_by' => 'repeat_interval',
637 'use_pricing_table' => 'yes'
638 ]),
639 'default_variation_id' => $variationId,
640 'default_media' => null
641 ];
642 $this->updateProductDetails($createdPostId, $detail);
643
644 // Handle downloadable files for simple product
645 if ($fulfillmentType === 'digital') {
646 $simpleDownloadableFiles = get_post_meta($wooProduct->ID, '_downloadable_files', true);
647 if ($simpleDownloadableFiles) {
648 $this->migrateDownloadableFiles($createdPostId, [$variationId => $simpleDownloadableFiles]);
649 }
650 }
651 }
652 return $createdPostId;
653 } catch (\Exception $e) {
654 return new \WP_Error('migration_failed', $e->getMessage());
655 }
656 }
657
658 /**
659 * Create or update a FluentCart product variation.
660 *
661 * Ensures the variation is created or updated with the correct price, stock, fulfillment type, downloadable flag, and meta.
662 * Also creates product meta for variation images if present.
663 *
664 * @param int $productId The FluentCart product ID
665 * @param array $data The variation data
666 * @return int The FluentCart variation ID
667 */
668 private function createOrUpdateProductVariations($productId, $data)
669 {
670 global $wpdb;
671
672 // Check if variation exists
673 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
674 $existingVariation = $wpdb->get_row(
675 $wpdb->prepare(
676 "SELECT * FROM {$wpdb->prefix}fct_product_variations WHERE post_id = %d AND variation_identifier = %s",
677 $productId,
678 $data['variation_identifier']
679 )
680 );
681
682 // Prices should already be converted to cents before reaching this function
683
684 $variationId = 0;
685
686 if ($existingVariation) {
687 // Update existing variation
688 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
689 $wpdb->update(
690 $wpdb->prefix . 'fct_product_variations',
691 array_merge($data, ['post_id' => $productId]),
692 ['id' => $existingVariation->id]
693 );
694 $variationId = $existingVariation->id;
695 } else {
696 // Insert new variation
697 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
698 $wpdb->insert(
699 $wpdb->prefix . 'fct_product_variations',
700 array_merge($data, ['post_id' => $productId])
701 );
702 $variationId = $wpdb->insert_id;
703 }
704
705 // Create product meta for variation image if media_id is set
706 if (!empty($data['media_id'])) {
707 $this->createVariationImageMeta($variationId, $data['media_id'], $data['variation_title']);
708 }
709
710 return $variationId;
711 }
712
713 /**
714 * Create or update product meta for a variation image.
715 *
716 * Links a media attachment to a variation for use as its thumbnail in FluentCart.
717 *
718 * @param int $variationId The variation ID
719 * @param int $mediaId The media/attachment ID
720 * @param string $variationTitle The variation title
721 */
722 private function createVariationImageMeta($variationId, $mediaId, $variationTitle)
723 {
724 global $wpdb;
725
726 // Get image details
727 $imageUrl = wp_get_attachment_url($mediaId);
728 $imageTitle = get_the_title($mediaId);
729
730 if (!$imageUrl) {
731 return;
732 }
733
734 // Prepare the meta value array (same structure as manually created)
735 $metaValue = [
736 [
737 'id' => (int)$mediaId,
738 'title' => $imageTitle ?: $variationTitle,
739 'url' => $imageUrl
740 ]
741 ];
742
743 // Check if meta already exists
744 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
745 $existingMeta = $wpdb->get_row($wpdb->prepare(
746 "SELECT * FROM {$wpdb->prefix}fct_product_meta WHERE object_id = %d AND object_type = 'product_variant_info' AND meta_key = 'product_thumbnail'",
747 $variationId
748 ));
749
750 if ($existingMeta) {
751 // Update existing meta
752 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
753 $wpdb->update(
754 $wpdb->prefix . 'fct_product_meta',
755 [
756 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
757 'meta_value' => serialize($metaValue),
758 'updated_at' => current_time('mysql')
759 ],
760 [
761 'object_id' => $variationId,
762 'object_type' => 'product_variant_info',
763 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
764 'meta_key' => 'product_thumbnail'
765 ]
766 );
767 } else {
768 // Insert new meta
769 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
770 $wpdb->insert(
771 $wpdb->prefix . 'fct_product_meta',
772 [
773 'object_id' => $variationId,
774 'object_type' => 'product_variant_info',
775 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
776 'meta_key' => 'product_thumbnail',
777 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
778 'meta_value' => serialize($metaValue),
779 'created_at' => current_time('mysql'),
780 'updated_at' => current_time('mysql')
781 ]
782 );
783 }
784
785 }
786
787 /**
788 * Update or insert product details for a FluentCart product.
789 *
790 * Handles the main product details row, including fulfillment type, stock, downloadable flag, and meta.
791 *
792 * @param int $createdPostId The FluentCart product ID
793 * @param array $detail The product details data
794 * @return int The FluentCart product details ID
795 */
796 private function updateProductDetails($createdPostId, $detail)
797 {
798 global $wpdb;
799
800 // Check if product details exist
801 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
802 $existingDetails = $wpdb->get_row($wpdb->prepare(
803 "SELECT * FROM {$wpdb->prefix}fct_product_details WHERE post_id = %d",
804 $createdPostId
805 ));
806
807 if ($existingDetails) {
808 // Update existing details
809 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
810 $wpdb->update(
811 $wpdb->prefix . 'fct_product_details',
812 $detail,
813 ['post_id' => $createdPostId]
814 );
815 return $existingDetails->id;
816 }
817
818 // Insert new details
819 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
820 $wpdb->insert(
821 $wpdb->prefix . 'fct_product_details',
822 $detail
823 );
824
825 return $wpdb->insert_id;
826 }
827
828 /**
829 * Convert a price to cents (integer) for FluentCart storage.
830 *
831 * Ensures all prices are stored as integer cents, not floats.
832 *
833 * @param float|string $price The price value
834 * @return int The price in cents
835 */
836 private function convertToCents($price)
837 {
838 if (empty($price)) {
839 return 0;
840 }
841 // Just cast to float and multiply by 100
842 return round(floatval($price) * 100);
843 }
844
845 /**
846 * Migrate downloadable files from WooCommerce to FluentCart.
847 *
848 * For each downloadable file, copies it to the FluentCart uploads directory, creates a download entry,
849 * and links it to the correct product variations. Handles file path resolution for WooCommerce's storage format.
850 *
851 * @param int $productId The FluentCart product ID
852 * @param array $variationDownloadMap Array mapping variation IDs to their downloadable files
853 * @return bool True on success
854 */
855 private function migrateDownloadableFiles($productId, $variationDownloadMap = [])
856 {
857 if (empty($variationDownloadMap)) {
858 return true;
859 }
860
861 global $wpdb;
862
863 // Group variations by their downloadable files
864 $downloadGroups = [];
865 foreach ($variationDownloadMap as $variationId => $downloadableFiles) {
866 if (empty($downloadableFiles)) {
867 continue;
868 }
869
870 // For each file, create a download group
871 foreach ($downloadableFiles as $fileId => $file) {
872 $fileKey = $file['file']; // Use file path as key to group identical files
873
874 if (!isset($downloadGroups[$fileKey])) {
875 $downloadGroups[$fileKey] = [
876 'file' => $file,
877 'variation_ids' => []
878 ];
879 }
880 $downloadGroups[$fileKey]['variation_ids'][] = $variationId;
881 }
882 }
883
884 // Delete existing downloads for this product
885 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
886 $wpdb->delete(
887 $wpdb->prefix . 'fct_product_downloads',
888 ['post_id' => $productId]
889 );
890
891 // Create download entries for each unique file
892 foreach ($downloadGroups as $fileKey => $downloadGroup) {
893 $file = $downloadGroup['file'];
894 $variationIds = $downloadGroup['variation_ids'];
895
896 // Get file details
897 $fileName = basename($file['file']);
898 $fileSize = $this->getFileSize($file['file']);
899 $fileType = $this->getFileType($fileName);
900
901 // Copy file to FluentCart uploads directory if not already present
902 $uploadDir = wp_upload_dir();
903 $sourceFile = $file['file'];
904
905 // If it's a URL, convert to local path
906 if (filter_var($sourceFile, FILTER_VALIDATE_URL)) {
907 $uploadsBaseUrl = $uploadDir['baseurl'];
908 $uploadsBaseDir = $uploadDir['basedir'];
909 if (strpos($sourceFile, $uploadsBaseUrl) === 0) {
910 $sourceFile = $uploadsBaseDir . substr($sourceFile, strlen($uploadsBaseUrl));
911 }
912 }
913
914 // If it's a relative path, prepend uploads basedir
915 if (!file_exists($sourceFile) && strpos($sourceFile, '/') !== 0 && strpos($sourceFile, ':') === false) {
916 $possibleSource = $uploadDir['basedir'] . '/' . ltrim($sourceFile, '/');
917 if (file_exists($possibleSource)) {
918 $sourceFile = $possibleSource;
919 }
920 }
921
922 $destDir = $uploadDir['basedir'] . '/fluent-cart/';
923 if (!file_exists($destDir)) {
924 wp_mkdir_p($destDir);
925 }
926 $destFile = $destDir . $fileName;
927 // Only copy if source exists and destination doesn't
928 if (file_exists($sourceFile) && !file_exists($destFile)) {
929 copy($sourceFile, $destFile);
930 }
931
932 // Generate unique download identifier
933 $downloadIdentifier = wp_generate_uuid4();
934
935 // Prepare settings
936 $settings = json_encode([
937 'download_limit' => '',
938 'download_expiry' => '',
939 'bucket' => ['400' => ['Invalid Credential']]
940 ]);
941
942 $downloadData = [
943 'post_id' => $productId,
944 'product_variation_id' => json_encode($variationIds),
945 'download_identifier' => $downloadIdentifier,
946 'title' => $file['name'] ?: $fileName,
947 'type' => $fileType,
948 'driver' => 'local',
949 'file_name' => $fileName,
950 'file_path' => $fileName, // Store only filename, not full path
951 'file_url' => $fileName, // Store only filename, not full path
952 'file_size' => $fileSize,
953 'settings' => $settings,
954 'serial' => 1,
955 'created_at' => current_time('mysql'),
956 'updated_at' => current_time('mysql')
957 ];
958
959 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
960 $wpdb->insert(
961 $wpdb->prefix . 'fct_product_downloads',
962 $downloadData
963 );
964
965 $downloadId = $wpdb->insert_id;
966 }
967
968 return true;
969 }
970
971 /**
972 * Get file size in bytes for a given file path or URL.
973 *
974 * Used for populating the file_size field in FluentCart's downloads table.
975 *
976 * @param string $filePath File path or URL
977 * @return string File size
978 */
979 private function getFileSize($filePath)
980 {
981 // If it's a URL, try to get file size
982 if (filter_var($filePath, FILTER_VALIDATE_URL)) {
983 $headers = get_headers($filePath, 1);
984 if (isset($headers['Content-Length'])) {
985 return $headers['Content-Length'];
986 }
987 }
988
989 // If it's a local file path
990 if (file_exists($filePath)) {
991 return filesize($filePath);
992 }
993
994 // Default size
995 return '102713';
996 }
997
998 /**
999 * Get file type based on file extension.
1000 *
1001 * Used for populating the type field in FluentCart's downloads table.
1002 *
1003 * @param string $fileName File name
1004 * @return string File type
1005 */
1006 private function getFileType($fileName)
1007 {
1008 $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
1009
1010 $typeMap = [
1011 'pdf' => 'pdf',
1012 'doc' => 'doc',
1013 'docx' => 'docx',
1014 'txt' => 'txt',
1015 'zip' => 'zip',
1016 'rar' => 'rar',
1017 'mp3' => 'mp3',
1018 'mp4' => 'mp4',
1019 'jpg' => 'jpg',
1020 'jpeg' => 'jpeg',
1021 'png' => 'png',
1022 'gif' => 'gif'
1023 ];
1024
1025 return isset($typeMap[$extension]) ? $typeMap[$extension] : 'file';
1026 }
1027
1028 /**
1029 * Generalized taxonomy migration from WooCommerce to FluentCart.
1030 *
1031 * @param string $sourceTaxonomy WooCommerce taxonomy (e.g., 'product_cat', 'product_brand')
1032 * @param string $destTaxonomy FluentCart taxonomy (e.g., 'product-categories', 'product-brands')
1033 * @param string $optionMapKey Option key for storing the term ID map
1034 * @return array Term ID map
1035 */
1036 private function migrateTaxonomy($sourceTaxonomy, $destTaxonomy, $optionMapKey)
1037 {
1038 if (!taxonomy_exists($destTaxonomy)) {
1039 // Register taxonomy if needed
1040 // $fluentProducts = new \FluentCart\App\CPT\FluentProducts();
1041 // $fluentProducts->registerProductTaxonomies();
1042 }
1043
1044 $terms = get_terms([
1045 'taxonomy' => $sourceTaxonomy,
1046 'hide_empty' => false
1047 ]);
1048
1049 if (is_wp_error($terms)) {
1050 return $terms;
1051 }
1052
1053 $termMap = get_option($optionMapKey, []);
1054
1055 foreach ($terms as $term) {
1056 $existing = get_term_by('slug', $term->slug, $destTaxonomy);
1057 if ($existing) {
1058 $termMap[$term->term_id] = $existing->term_id;
1059 continue;
1060 }
1061 $result = wp_insert_term(
1062 $term->name,
1063 $destTaxonomy,
1064 [
1065 'description' => $term->description,
1066 'slug' => $term->slug,
1067 'parent' => 0 // Add parent mapping if needed
1068 ]
1069 );
1070 if (!is_wp_error($result)) {
1071 $termMap[$term->term_id] = $result['term_id'];
1072 }
1073 }
1074
1075 update_option($optionMapKey, $termMap);
1076 return $termMap;
1077 }
1078 }
1079