| 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 |
|