diff --git a/app/Http/Controllers/MagentoMigrationController.php b/app/Http/Controllers/MagentoMigrationController.php index ae8e16e..62e0f70 100644 --- a/app/Http/Controllers/MagentoMigrationController.php +++ b/app/Http/Controllers/MagentoMigrationController.php @@ -32,6 +32,10 @@ public function index() $m1AttributeGroups = $this->migrationService->getMagento1AttributeGroups(); $m2AttributeGroups = $this->migrationService->getMagento2AttributeGroups(); $m1AttributeGroupsMissingInM2 = $this->migrationService->getM1AttributeGroupsMissingInM2(); + $m1Products = $this->migrationService->getMagento1Products(); + $m2Products = $this->migrationService->getMagento2Products(); + $m1ProductsNotInM2 = $this->migrationService->getM1ProductsNotInM2(); + $m2ProductsNotInM1 = $this->migrationService->getM2ProductsNotInM1(); return view('migration.index', [ 'm1Stores' => $m1Stores, @@ -46,6 +50,10 @@ public function index() 'm1AttributeGroups' => $m1AttributeGroups, 'm2AttributeGroups' => $m2AttributeGroups, 'm1AttributeGroupsMissingInM2' => $m1AttributeGroupsMissingInM2, + 'm1Products' => $m1Products, + 'm2Products' => $m2Products, + 'm1ProductsNotInM2' => $m1ProductsNotInM2, + 'm2ProductsNotInM1' => $m2ProductsNotInM1, ]); } @@ -106,6 +114,50 @@ public function getMagento1CategoryTree() } } + /** + * Get Magento 1 category tree with products + */ + public function getMagento1CategoryTreeWithProducts() + { + try { + $tree = $this->migrationService->getMagento1CategoryTreeWithProducts(); + + return response()->json([ + 'success' => true, + 'tree' => $tree + ]); + } catch (\Exception $e) { + Log::error('Error fetching M1 category tree with products: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to load category tree with products: ' . $e->getMessage(), + 'tree' => [] + ], 500); + } + } + + /** + * Get Magento 2 category tree with products + */ + public function getMagento2CategoryTreeWithProducts() + { + try { + $tree = $this->migrationService->getMagento2CategoryTreeWithProducts(); + + return response()->json([ + 'success' => true, + 'tree' => $tree + ]); + } catch (\Exception $e) { + Log::error('Error fetching M2 category tree with products: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to load category tree with products: ' . $e->getMessage(), + 'tree' => [] + ], 500); + } + } + /** * Get Magento 2 category tree */ @@ -220,6 +272,97 @@ public function migrateAttributeGroup(Request $request, $groupId, $setId) } } + /** + * Migrate all products from Magento 1 to Magento 2 + */ + public function migrateProducts(Request $request) + { + try { + $dryRun = $request->input('dry_run', false); + $result = $this->migrationService->migrateProducts($dryRun); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Product migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ], 500); + } + } + + /** + * Delete a single product from Magento 2 + */ + public function deleteM2Product(Request $request, $productId) + { + try { + $result = $this->migrationService->deleteM2Product($productId); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 product error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage() + ], 500); + } + } + + /** + * Sync product category assignments from M1 to M2 + */ + public function syncProductCategories(Request $request) + { + try { + $result = $this->migrationService->syncProductCategories(); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Sync product categories error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Sync failed: ' . $e->getMessage(), + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } + + /** + * Delete all products in Magento 2 that have entity_id greater than max M1 product ID + */ + public function deleteM2ProductsAboveM1Max(Request $request) + { + try { + $result = $this->migrationService->deleteM2ProductsAboveM1Max(); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Delete M2 products error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Deletion failed: ' . $e->getMessage(), + 'deleted' => 0 + ], 500); + } + } + /** * Execute the migration */ diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index df05442..39dc887 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -1479,6 +1479,1805 @@ public function migrateAttributeGroup($m1GroupId, $m1SetId) } } + /** + * Get all products from Magento 1 + */ + public function getMagento1Products() + { + try { + // Get entity type ID for catalog_product + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + // Get base product data - check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + // SKU column doesn't exist, will use EAV + $hasSkuColumn = false; + } + + if ($hasSkuColumn) { + // SKU is stored directly in entity table + $products = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } else { + // SKU is stored as EAV attribute + $products = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('entity_id', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } + + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKU and name values from EAV if needed + $skus = []; + $names = []; + + if (!$hasSkuColumn && $skuAttributeId) { + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Combine data + foreach ($products as $product) { + if ($hasSkuColumn) { + // SKU from entity table + $product->sku = !empty($product->sku) ? $product->sku : 'N/A'; + } else { + // SKU from EAV + $product->sku = $skus[$product->entity_id] ?? 'N/A'; + } + $product->name = $names[$product->entity_id] ?? 'Unnamed Product'; + } + + return $products; + } catch (Exception $e) { + Log::error('Error fetching Magento 1 products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get all products from Magento 2 + */ + public function getMagento2Products() + { + try { + // Get entity type ID for catalog_product + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + // Get base product data - check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + // SKU column doesn't exist, will use EAV + $hasSkuColumn = false; + } + + if ($hasSkuColumn) { + // SKU is stored directly in entity table + $products = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } else { + // SKU is stored as EAV attribute + $products = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('entity_id', 'type_id', 'attribute_set_id', 'created_at', 'updated_at') + ->orderBy('entity_id') + ->get(); + } + + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKU and name values from EAV if needed + $skus = []; + $names = []; + + if (!$hasSkuColumn && $skuAttributeId) { + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Combine data + foreach ($products as $product) { + if ($hasSkuColumn) { + // SKU from entity table + $product->sku = !empty($product->sku) ? $product->sku : 'N/A'; + } else { + // SKU from EAV + $product->sku = $skus[$product->entity_id] ?? 'N/A'; + } + $product->name = $names[$product->entity_id] ?? 'Unnamed Product'; + } + + return $products; + } catch (Exception $e) { + Log::error('Error fetching Magento 2 products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get Magento 1 products that don't exist in Magento 2 + */ + public function getM1ProductsNotInM2() + { + try { + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Get all M2 SKUs + $m2Skus = $m2Products->pluck('sku')->filter(function($sku) { + return $sku !== 'N/A' && !empty($sku); + })->toArray(); + + // Get M2 product IDs (for products without SKU) + $m2ProductIds = $m2Products->pluck('entity_id')->toArray(); + + // Filter M1 products that don't exist in M2 + $missingProducts = $m1Products->filter(function ($m1Product) use ($m2Skus, $m2ProductIds) { + $m1Sku = $m1Product->sku ?? 'N/A'; + $hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku)); + + if ($hasSku) { + // Check by SKU + return !in_array($m1Sku, $m2Skus); + } else { + // Check by product ID + return !in_array($m1Product->entity_id, $m2ProductIds); + } + }); + + return $missingProducts->values(); + } catch (Exception $e) { + Log::error('Error fetching missing products: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Get Magento 2 products that don't exist in Magento 1 + */ + public function getM2ProductsNotInM1() + { + try { + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Get all M1 SKUs + $m1Skus = $m1Products->pluck('sku')->filter(function($sku) { + return $sku !== 'N/A' && !empty($sku); + })->toArray(); + + // Get M1 product IDs (for products without SKU) + $m1ProductIds = $m1Products->pluck('entity_id')->toArray(); + + // Filter M2 products that don't exist in M1 + $missingProducts = $m2Products->filter(function ($m2Product) use ($m1Skus, $m1ProductIds) { + $m2Sku = $m2Product->sku ?? 'N/A'; + $hasSku = ($m2Sku !== 'N/A' && !empty($m2Sku)); + + if ($hasSku) { + // Check by SKU + return !in_array($m2Sku, $m1Skus); + } else { + // Check by product ID + return !in_array($m2Product->entity_id, $m1ProductIds); + } + }); + + return $missingProducts->values(); + } catch (Exception $e) { + Log::error('Error fetching M2 products not in M1: ' . $e->getMessage()); + return collect([]); + } + } + + /** + * Migrate all products from Magento 1 to Magento 2 + */ + public function migrateProducts($dryRun = false) + { + try { + $this->migrationLog = []; + $addedCount = 0; + $updatedCount = 0; + $errorCount = 0; + $missingAttributes = []; + + // Get M1 and M2 entity type IDs + $m1EntityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + $m2EntityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$m1EntityTypeId || !$m2EntityTypeId) { + return [ + 'success' => false, + 'message' => 'Entity type not found', + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ]; + } + + // Get all M1 products + $m1Products = $this->getMagento1Products(); + + // Get M2 attribute IDs (common ones) + $m2AttributeIds = $this->getMagento2ProductAttributeIds(); + + // Get M1 attribute IDs for common attributes + $m1AttributeIds = $this->getMagento1ProductAttributeIds(); + + // Get all M1 attributes to check for missing ones + $allM1Attributes = $this->getAllMagento1ProductAttributes(); + + // Also get all M1 attribute IDs (not just common ones) for comprehensive checking + $allM1AttributeIds = []; + if ($allM1Attributes) { + foreach ($allM1Attributes as $attr) { + $allM1AttributeIds[$attr->attribute_code] = $attr->attribute_id; + } + } + + // Get all M2 attribute IDs for comparison + $allM2AttributeIds = $this->getAllMagento2ProductAttributeIds(); + + // Get category mapping (from previous category migrations) + $categoryMapping = $this->getCategoryMapping(); + + if (!$dryRun) { + DB::connection($this->magento2Connection)->beginTransaction(); + } + + // Check if M2 has SKU column in entity table + $m2HasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $m2HasSkuColumn = true; + } catch (Exception $e) { + $m2HasSkuColumn = false; + } + + foreach ($m1Products as $m1Product) { + try { + $m1Sku = $m1Product->sku ?? 'N/A'; + $hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku)); + + // Check if product exists in M2 by SKU (if SKU exists) or by ID (if no SKU) + $m2Product = null; + $m2ProductId = null; + $isNew = false; + + if ($hasSku) { + // Check if SKU exists in M2 - try both entity table and EAV + if ($m2HasSkuColumn) { + // SKU is stored in entity table + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('sku', $m1Sku) + ->first(); + } else { + // SKU is stored in EAV table + if (isset($m2AttributeIds['sku'])) { + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $m2AttributeIds['sku']) + ->where('value', $m1Sku) + ->where('store_id', 0) + ->first(); + } + } + } else { + // Product has no SKU, check by entity_id + $m2Product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $m1Product->entity_id) + ->first(); + } + + if ($m2Product) { + // Product exists, get its entity_id + $m2ProductId = $m2Product->entity_id; + if (!$dryRun) { + $this->migrationLog[] = "Updating existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})"; + } else { + $this->migrationLog[] = "Would update existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})"; + } + $updatedCount++; + } else { + // Product doesn't exist + if ($hasSku) { + // Product has SKU, will get new ID + if (!$dryRun) { + // Create new product - check if we need to set SKU in entity table or EAV + if ($m2HasSkuColumn) { + // SKU goes in entity table + $m2ProductId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insertGetId([ + 'sku' => $m1Sku, + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]); + } else { + // SKU goes in EAV table + $m2ProductId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insertGetId([ + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]); + + // Insert SKU in EAV table + if (isset($m2AttributeIds['sku'])) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->insert([ + 'attribute_id' => $m2AttributeIds['sku'], + 'store_id' => 0, + 'entity_id' => $m2ProductId, + 'value' => $m1Sku, + ]); + } + } + $this->migrationLog[] = "Created new product: SKU {$m1Sku} (ID: {$m2ProductId})"; + } else { + $m2ProductId = $m1Product->entity_id; // Use M1 ID for dry run simulation + $this->migrationLog[] = "Would create new product: SKU {$m1Sku} (ID: {$m2ProductId})"; + } + } else { + // Product has no SKU, use M1 product ID + $m2ProductId = $m1Product->entity_id; + if (!$dryRun) { + // Check if this ID already exists in M2 + $existing = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $m2ProductId) + ->exists(); + + if (!$existing) { + $insertData = [ + 'entity_id' => $m2ProductId, + 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, + 'type_id' => $m1Product->type_id ?? 'simple', + 'created_at' => $m1Product->created_at ?? now(), + 'updated_at' => now(), + ]; + + // If SKU column exists, set it to NULL or empty + if ($m2HasSkuColumn) { + $insertData['sku'] = null; + } + + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->insert($insertData); + $this->migrationLog[] = "Created new product: No SKU (ID: {$m2ProductId}, using M1 ID)"; + } else { + $this->migrationLog[] = "Updating existing product: No SKU (ID: {$m2ProductId})"; + $updatedCount++; + $addedCount--; // Adjust counts + continue; + } + } else { + $this->migrationLog[] = "Would create new product: No SKU (ID: {$m2ProductId}, using M1 ID)"; + } + } + $isNew = true; + $addedCount++; + } + + // Migrate product attributes (with dry run support) + // Use all M1 attributes for comprehensive checking, not just common ones + $missingAttrs = $this->migrateProductAttributes($m1Product->entity_id, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $allM1AttributeIds, $allM2AttributeIds, $isNew, $dryRun, $allM1Attributes); + if ($missingAttrs) { + foreach ($missingAttrs as $attr) { + // Check if this attribute is already in the list + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $attr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $attr; + } + } + } + + // Migrate category associations (skip in dry run) + if (!$dryRun) { + // Ensure product is enabled and visible (required for products to show in categories after reindex) + $this->ensureProductIsEnabledAndVisible($m2ProductId); + $this->migrateProductCategories($m1Product->entity_id, $m2ProductId, $categoryMapping); + } + + } catch (Exception $e) { + $errorCount++; + $errorMsg = $e->getMessage(); + Log::error("Error migrating product {$m1Product->entity_id} (SKU: " . ($m1Product->sku ?? 'N/A') . "): " . $errorMsg); + $this->migrationLog[] = "ERROR: Failed to migrate product SKU " . ($m1Product->sku ?? 'N/A') . ": " . $errorMsg; + + // Try to extract missing attribute from error message + if (preg_match("/Table.*catalog_product_entity_(\w+).*doesn't exist/", $errorMsg, $matches)) { + $tableType = $matches[1] ?? null; + if ($tableType && $allM1Attributes) { + // Find attributes that use this backend type + foreach ($allM1Attributes as $attr) { + if ($attr->backend_type === $tableType) { + $missingAttr = [ + 'code' => $attr->attribute_code, + 'label' => $attr->frontend_label ?? $attr->attribute_code, + 'type' => $attr->backend_type + ]; + // Check if already exists + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $missingAttr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $missingAttr; + } + } + } + } + } + } + } + + if (!$dryRun) { + DB::connection($this->magento2Connection)->commit(); + } + + return [ + 'success' => true, + 'message' => $dryRun ? 'Product migration dry run completed' : 'Product migration completed', + 'added' => $addedCount, + 'updated' => $updatedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog, + 'missing_attributes' => $missingAttributes, + 'dry_run' => $dryRun + ]; + + } catch (Exception $e) { + if (!$dryRun && isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Product migration error: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Product migration failed: ' . $e->getMessage(), + 'added' => 0, + 'updated' => 0, + 'errors' => 0, + 'log' => [], + 'missing_attributes' => [] + ]; + } + } + + /** + * Get Magento 1 product attribute IDs + */ + protected function getMagento1ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id']; + $attributeIds = []; + + foreach ($attributes as $attrCode) { + $attrId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + if ($attrId) { + $attributeIds[$attrCode] = $attrId; + } + } + + return $attributeIds; + } + + /** + * Get all Magento 1 product attributes + */ + protected function getAllMagento1ProductAttributes() + { + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return collect([]); + } + + return DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label') + ->get(); + } + + /** + * Get all Magento 2 product attribute IDs + */ + protected function getAllMagento2ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->select('attribute_id', 'attribute_code') + ->get(); + + $attributeIds = []; + foreach ($attributes as $attr) { + $attributeIds[$attr->attribute_code] = $attr->attribute_id; + } + + return $attributeIds; + } + + /** + * Get Magento 2 product attribute IDs + */ + protected function getMagento2ProductAttributeIds() + { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return []; + } + + $attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id']; + $attributeIds = []; + + foreach ($attributes as $attrCode) { + $attrId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + if ($attrId) { + $attributeIds[$attrCode] = $attrId; + } + } + + return $attributeIds; + } + + /** + * Migrate product attributes from M1 to M2 + */ + protected function migrateProductAttributes($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $isNew, $dryRun = false, $allM1Attributes = null) + { + $missingAttributes = []; + + // Get attribute backend types for all M1 attributes + $m1AttributeTypes = []; + if (!empty($m1AttributeIds)) { + $m1AttributeTypes = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $m1EntityTypeId) + ->whereIn('attribute_id', array_values($m1AttributeIds)) + ->pluck('backend_type', 'attribute_id') + ->toArray(); + } + + $m2AttributeTypes = []; + if (!empty($m2AttributeIds)) { + $m2AttributeTypes = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $m2EntityTypeId) + ->whereIn('attribute_id', array_values($m2AttributeIds)) + ->pluck('backend_type', 'attribute_id') + ->toArray(); + } + + // Migrate each attribute + foreach ($m1AttributeIds as $attrCode => $m1AttrId) { + // Check if attribute exists in M2 + if (!isset($m2AttributeIds[$attrCode])) { + // Attribute doesn't exist in M2 + if ($allM1Attributes) { + $attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId); + if ($attr) { + $missingAttributes[] = [ + 'code' => $attrCode, + 'label' => $attr->frontend_label ?? $attrCode, + 'type' => $attr->backend_type ?? 'varchar' + ]; + } + } + continue; + } + + $m2AttrId = $m2AttributeIds[$attrCode]; + $backendType = $m1AttributeTypes[$m1AttrId] ?? 'varchar'; + + // Handle static attributes (stored in main entity table) + if ($backendType === 'static') { + // Static attributes are columns in catalog_product_entity table + // Skip for now as they're usually handled during entity creation + continue; + } + + // Get M1 attribute value + $m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType; + + try { + $m1Value = DB::connection($this->magento1Connection) + ->table($m1Table) + ->where('entity_id', $m1ProductId) + ->where('attribute_id', $m1AttrId) + ->where('store_id', 0) + ->value('value'); + } catch (Exception $e) { + // Table doesn't exist - this means the attribute table is missing + // Track this as a missing attribute/table + if ($allM1Attributes) { + $attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId); + if ($attr) { + $missingAttr = [ + 'code' => $attrCode, + 'label' => $attr->frontend_label ?? $attrCode, + 'type' => $backendType + ]; + // Check if already in list + $exists = false; + foreach ($missingAttributes as $existingAttr) { + if ($existingAttr['code'] === $missingAttr['code']) { + $exists = true; + break; + } + } + if (!$exists) { + $missingAttributes[] = $missingAttr; + } + } + } + Log::warning("Table {$m1Table} doesn't exist for attribute {$attrCode}: " . $e->getMessage()); + continue; + } + + if ($m1Value === null) { + continue; // No value in M1 + } + + if (!$dryRun) { + // Insert or update in M2 + $m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType; + $exists = DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', 0) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($m2Table) + ->where('entity_id', $m2ProductId) + ->where('attribute_id', $m2AttrId) + ->where('store_id', 0) + ->update(['value' => $m1Value]); + } else { + DB::connection($this->magento2Connection) + ->table($m2Table) + ->insert([ + 'attribute_id' => $m2AttrId, + 'store_id' => 0, + 'entity_id' => $m2ProductId, + 'value' => $m1Value, + ]); + } + } + } + + return $missingAttributes; + } + + /** + * Delete a single product from Magento 2 + */ + public function deleteM2Product($productId) + { + try { + // Check if product exists + $product = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->first(); + + if (!$product) { + return [ + 'success' => false, + 'message' => 'Product not found in Magento 2' + ]; + } + + DB::connection($this->magento2Connection)->beginTransaction(); + + try { + // Delete product attributes + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType; + try { + DB::connection($this->magento2Connection) + ->table($table) + ->where('entity_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$table} might not exist: " . $e->getMessage()); + } + } + + // Delete category associations + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage()); + } + + // Delete stock items if they exist + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'cataloginventory_stock_item') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + } + + // Delete the product entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->delete(); + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Product ID {$productId} deleted successfully" + ]; + + } catch (Exception $e) { + DB::connection($this->magento2Connection)->rollBack(); + throw $e; + } + + } catch (Exception $e) { + Log::error('Error deleting M2 product: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete product: ' . $e->getMessage() + ]; + } + } + + /** + * Sync product category assignments from M1 to M2 + * Updates M2 products to be in the same categories as their M1 counterparts + */ + public function syncProductCategories() + { + try { + $this->migrationLog = []; + $updatedCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // Get category mapping + $categoryMapping = $this->getCategoryMapping(); + + if (empty($categoryMapping)) { + return [ + 'success' => false, + 'message' => 'No category mapping found. Please migrate categories first.', + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + + // Get all M1 products with their category associations + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Create lookup maps for M2 products + $m2ProductBySku = []; + $m2ProductById = []; + foreach ($m2Products as $m2Product) { + $sku = $m2Product->sku ?? 'N/A'; + if ($sku !== 'N/A' && !empty($sku)) { + $m2ProductBySku[$sku] = $m2Product->entity_id; + } + $m2ProductById[$m2Product->entity_id] = $m2Product->entity_id; + } + + DB::connection($this->magento2Connection)->beginTransaction(); + + foreach ($m1Products as $m1Product) { + try { + // Find corresponding M2 product + $m2ProductId = null; + $m1Sku = $m1Product->sku ?? 'N/A'; + + if ($m1Sku !== 'N/A' && !empty($m1Sku)) { + // Match by SKU + if (isset($m2ProductBySku[$m1Sku])) { + $m2ProductId = $m2ProductBySku[$m1Sku]; + } + } else { + // Match by product ID + if (isset($m2ProductById[$m1Product->entity_id])) { + $m2ProductId = $m1Product->entity_id; + } + } + + if (!$m2ProductId) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - not found in M2"; + continue; + } + + // Ensure product is enabled and visible (required for products to show in categories after reindex) + $this->ensureProductIsEnabledAndVisible($m2ProductId); + + // Get M1 product categories + $m1Categories = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->where('product_id', $m1Product->entity_id) + ->get(); + + if ($m1Categories->isEmpty()) { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - no categories in M1"; + continue; + } + + // Get current M2 product categories + $m2CurrentCategories = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $m2ProductId) + ->pluck('category_id') + ->toArray(); + + // Build expected M2 categories from M1 categories using mapping + $expectedM2Categories = []; + $categoriesToAdd = []; + $unmappedCategories = []; + + foreach ($m1Categories as $m1Category) { + if (isset($categoryMapping[$m1Category->category_id])) { + $m2CategoryId = $categoryMapping[$m1Category->category_id]; + $expectedM2Categories[] = $m2CategoryId; + + // Check if this category association needs to be added + if (!in_array($m2CategoryId, $m2CurrentCategories)) { + $categoriesToAdd[] = [ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]; + } + } else { + // Category not in mapping - try to find it by name + $m1CategoryName = $this->getCategoryNameById($m1Category->category_id, 'm1'); + if ($m1CategoryName) { + $m2CategoryId = $this->findCategoryByName($m1CategoryName, 'm2'); + if ($m2CategoryId) { + // Found by name, add to mapping for future use + $categoryMapping[$m1Category->category_id] = $m2CategoryId; + $expectedM2Categories[] = $m2CategoryId; + + if (!in_array($m2CategoryId, $m2CurrentCategories)) { + $categoriesToAdd[] = [ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]; + } + $this->migrationLog[] = "FOUND BY NAME: M1 Category ID {$m1Category->category_id} ({$m1CategoryName}) -> M2 Category ID {$m2CategoryId}"; + } else { + $unmappedCategories[] = $m1CategoryName ?: "ID {$m1Category->category_id}"; + } + } else { + $unmappedCategories[] = "ID {$m1Category->category_id}"; + } + } + } + + if (!empty($unmappedCategories)) { + $this->migrationLog[] = "WARNING: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - M1 categories not found in M2: " . implode(', ', $unmappedCategories); + } + + // Remove categories that are in M2 but not in M1 (after mapping) + $categoriesToRemove = array_diff($m2CurrentCategories, $expectedM2Categories); + + // Add missing category associations + foreach ($categoriesToAdd as $categoryData) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->insert($categoryData); + } + + // Remove categories that shouldn't be there + if (!empty($categoriesToRemove)) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $m2ProductId) + ->whereIn('category_id', $categoriesToRemove) + ->delete(); + } + + if (!empty($categoriesToAdd) || !empty($categoriesToRemove)) { + $updatedCount++; + $addedCount = count($categoriesToAdd); + $removedCount = count($categoriesToRemove); + $this->migrationLog[] = "UPDATED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - Added {$addedCount} category(ies), Removed {$removedCount} category(ies)"; + } else { + $skippedCount++; + $this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - categories already match"; + } + + } catch (Exception $e) { + $errorCount++; + $m1Sku = $m1Product->sku ?? 'N/A'; + $this->migrationLog[] = "ERROR: Failed to sync categories for M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}): " . $e->getMessage(); + Log::error("Error syncing product categories for M1 Product ID {$m1Product->entity_id}: " . $e->getMessage()); + } + } + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Synced product categories. Updated: {$updatedCount}, Skipped: {$skippedCount}, Errors: {$errorCount}", + 'updated' => $updatedCount, + 'skipped' => $skippedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error syncing product categories: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to sync product categories: ' . $e->getMessage(), + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => $this->migrationLog + ]; + } + } + + /** + * Delete all products in Magento 2 that have entity_id greater than max M1 product ID + */ + public function deleteM2ProductsAboveM1Max() + { + try { + // Get max product ID from M1 + $maxM1ProductId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->max('entity_id'); + + if (!$maxM1ProductId) { + return [ + 'success' => false, + 'message' => 'No products found in Magento 1', + 'deleted' => 0 + ]; + } + + // Get products to delete from M2 + $productsToDelete = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', '>', $maxM1ProductId) + ->pluck('entity_id') + ->toArray(); + + if (empty($productsToDelete)) { + return [ + 'success' => true, + 'message' => 'No products found to delete', + 'deleted' => 0, + 'max_m1_id' => $maxM1ProductId + ]; + } + + $deletedCount = 0; + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + DB::connection($this->magento2Connection)->beginTransaction(); + + foreach ($productsToDelete as $productId) { + try { + // Delete product attributes + $attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime']; + foreach ($attributeTables as $tableType) { + $table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType; + try { + DB::connection($this->magento2Connection) + ->table($table) + ->where('entity_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + Log::warning("Table {$table} might not exist: " . $e->getMessage()); + } + } + + // Delete category associations + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage()); + } + + // Delete stock items if they exist + try { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'cataloginventory_stock_item') + ->where('product_id', $productId) + ->delete(); + } catch (Exception $e) { + // Table might not exist, continue + } + + // Delete the product entity + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $productId) + ->delete(); + + $deletedCount++; + } catch (Exception $e) { + Log::error("Error deleting product {$productId}: " . $e->getMessage()); + } + } + + DB::connection($this->magento2Connection)->commit(); + + return [ + 'success' => true, + 'message' => "Deleted {$deletedCount} product(s) with ID > {$maxM1ProductId}", + 'deleted' => $deletedCount, + 'max_m1_id' => $maxM1ProductId + ]; + + } catch (Exception $e) { + if (isset($this->magento2Connection)) { + DB::connection($this->magento2Connection)->rollBack(); + } + Log::error('Error deleting M2 products above M1 max: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Failed to delete products: ' . $e->getMessage(), + 'deleted' => 0 + ]; + } + } + + /** + * Migrate product category associations + */ + protected function migrateProductCategories($m1ProductId, $m2ProductId, $categoryMapping) + { + // Get M1 product categories + $m1Categories = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->where('product_id', $m1ProductId) + ->get(); + + foreach ($m1Categories as $m1Category) { + // Find corresponding M2 category + if (isset($categoryMapping[$m1Category->category_id])) { + $m2CategoryId = $categoryMapping[$m1Category->category_id]; + + // Check if association already exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->where('category_id', $m2CategoryId) + ->where('product_id', $m2ProductId) + ->exists(); + + if (!$exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->insert([ + 'category_id' => $m2CategoryId, + 'product_id' => $m2ProductId, + 'position' => $m1Category->position ?? 0, + ]); + } + } + } + } + + /** + * Ensure product is enabled and visible (required for products to appear in categories after reindex) + */ + protected function ensureProductIsEnabledAndVisible($productId) + { + try { + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return; + } + + // Get attribute IDs + $statusAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'status') + ->value('attribute_id'); + + $visibilityAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'visibility') + ->value('attribute_id'); + + // Set status to 1 (enabled) if not already set + if ($statusAttributeId) { + $statusTable = $this->magento2Prefix . 'catalog_product_entity_int'; + $hasStatus = DB::connection($this->magento2Connection) + ->table($statusTable) + ->where('entity_id', $productId) + ->where('attribute_id', $statusAttributeId) + ->where('store_id', 0) + ->exists(); + + if (!$hasStatus) { + DB::connection($this->magento2Connection) + ->table($statusTable) + ->insert([ + 'attribute_id' => $statusAttributeId, + 'store_id' => 0, + 'entity_id' => $productId, + 'value' => 1, // Enabled + ]); + } else { + // Update to enabled if it's disabled + DB::connection($this->magento2Connection) + ->table($statusTable) + ->where('entity_id', $productId) + ->where('attribute_id', $statusAttributeId) + ->where('store_id', 0) + ->update(['value' => 1]); + } + } + + // Set visibility to 4 (Catalog, Search) if not already set + if ($visibilityAttributeId) { + $visibilityTable = $this->magento2Prefix . 'catalog_product_entity_int'; + $hasVisibility = DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->exists(); + + if (!$hasVisibility) { + DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->insert([ + 'attribute_id' => $visibilityAttributeId, + 'store_id' => 0, + 'entity_id' => $productId, + 'value' => 4, // Catalog, Search + ]); + } else { + // Update to Catalog, Search if it's Not Visible or Search Only + $currentVisibility = DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->value('value'); + + // Only update if visibility is 1 (Not Visible) or 2 (Catalog) + // 3 = Search, 4 = Catalog, Search (both) + if ($currentVisibility == 1 || $currentVisibility == 2) { + DB::connection($this->magento2Connection) + ->table($visibilityTable) + ->where('entity_id', $productId) + ->where('attribute_id', $visibilityAttributeId) + ->where('store_id', 0) + ->update(['value' => 4]); + } + } + } + } catch (Exception $e) { + Log::warning("Error ensuring product {$productId} is enabled and visible: " . $e->getMessage()); + } + } + + /** + * Get Magento 1 category tree with products + */ + public function getMagento1CategoryTreeWithProducts() + { + try { + $categories = $this->getMagento1Categories(); + $categoryMap = []; + + // Build category map + foreach ($categories as $category) { + $categoryMap[$category->entity_id] = [ + 'id' => $category->entity_id, + 'name' => $category->name, + 'parent_id' => $category->parent_id, + 'is_active' => $category->is_active ?? true, + 'path' => $category->path ?? '', + 'children' => [], + 'products' => [] + ]; + } + + // Get all products in categories + $categoryProducts = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_category_product') + ->select('category_id', 'product_id', 'position') + ->orderBy('category_id') + ->orderBy('position') + ->get(); + + // Get product details + $productIds = $categoryProducts->pluck('product_id')->unique()->toArray(); + $products = []; + + if (!empty($productIds)) { + // Check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + $hasSkuColumn = false; + } + + $entityTypeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKUs - from entity table or EAV + $skus = []; + if ($hasSkuColumn) { + // SKU is stored in entity table + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity') + ->whereIn('entity_id', $productIds) + ->pluck('sku', 'entity_id') + ->toArray(); + $skus = $skuValues; + } else if ($skuAttributeId) { + // SKU is stored in EAV table + $skuValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + // Get names + $names = []; + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Build products array + foreach ($productIds as $productId) { + $sku = $skus[$productId] ?? 'N/A'; + $products[$productId] = [ + 'id' => $productId, + 'sku' => !empty($sku) ? $sku : 'N/A', + 'name' => $names[$productId] ?? 'Unnamed Product' + ]; + } + } + } + + // Assign products to categories + foreach ($categoryProducts as $cp) { + if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) { + $categoryMap[$cp->category_id]['products'][] = [ + 'id' => $products[$cp->product_id]['id'], + 'sku' => $products[$cp->product_id]['sku'], + 'name' => $products[$cp->product_id]['name'], + 'position' => $cp->position ?? 0 + ]; + } + } + + // Build tree structure + $tree = []; + foreach ($categoryMap as $categoryId => $category) { + if ($category['parent_id'] == 0 || $category['parent_id'] == 1) { + // Root category + $tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap); + } + } + + return $tree; + } catch (Exception $e) { + Log::error('Error fetching M1 category tree with products: ' . $e->getMessage()); + return []; + } + } + + /** + * Get Magento 2 category tree with products + */ + public function getMagento2CategoryTreeWithProducts() + { + try { + $categories = $this->getMagento2Categories(); + $categoryMap = []; + + // Build category map + foreach ($categories as $category) { + $categoryMap[$category->entity_id] = [ + 'id' => $category->entity_id, + 'name' => $category->name, + 'parent_id' => $category->parent_id, + 'is_active' => $category->is_active ?? true, + 'path' => $category->path ?? '', + 'children' => [], + 'products' => [] + ]; + } + + // Get all products in categories + $categoryProducts = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_category_product') + ->select('category_id', 'product_id', 'position') + ->orderBy('category_id') + ->orderBy('position') + ->get(); + + // Get product details + $productIds = $categoryProducts->pluck('product_id')->unique()->toArray(); + $products = []; + + if (!empty($productIds)) { + // Check if SKU column exists in entity table + $hasSkuColumn = false; + try { + $testQuery = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->select('sku') + ->limit(1) + ->first(); + $hasSkuColumn = true; + } catch (Exception $e) { + $hasSkuColumn = false; + } + + $entityTypeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if ($entityTypeId) { + // Get SKU attribute ID (for EAV storage) + $skuAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + // Get name attribute ID + $nameAttributeId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + // Get SKUs - from entity table or EAV + $skus = []; + if ($hasSkuColumn) { + // SKU is stored in entity table + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->whereIn('entity_id', $productIds) + ->pluck('sku', 'entity_id') + ->toArray(); + $skus = $skuValues; + } else if ($skuAttributeId) { + // SKU is stored in EAV table + $skuValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $skus = $skuValues; + } + + // Get names + $names = []; + if ($nameAttributeId) { + $nameValues = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereIn('entity_id', $productIds) + ->pluck('value', 'entity_id') + ->toArray(); + $names = $nameValues; + } + + // Build products array + foreach ($productIds as $productId) { + $sku = $skus[$productId] ?? 'N/A'; + $products[$productId] = [ + 'id' => $productId, + 'sku' => !empty($sku) ? $sku : 'N/A', + 'name' => $names[$productId] ?? 'Unnamed Product' + ]; + } + } + } + + // Assign products to categories + foreach ($categoryProducts as $cp) { + if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) { + $categoryMap[$cp->category_id]['products'][] = [ + 'id' => $products[$cp->product_id]['id'], + 'sku' => $products[$cp->product_id]['sku'], + 'name' => $products[$cp->product_id]['name'], + 'position' => $cp->position ?? 0 + ]; + } + } + + // Build tree structure + $tree = []; + foreach ($categoryMap as $categoryId => $category) { + if ($category['parent_id'] == 0 || $category['parent_id'] == 1) { + // Root category + $tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap); + } + } + + return $tree; + } catch (Exception $e) { + Log::error('Error fetching M2 category tree with products: ' . $e->getMessage()); + return []; + } + } + + /** + * Build category tree structure with products recursively + */ + protected function buildCategoryTreeWithProducts($category, $categoryMap) + { + $node = [ + 'id' => $category['id'], + 'name' => $category['name'], + 'is_active' => $category['is_active'], + 'products' => $category['products'], + 'children' => [] + ]; + + // Find children + foreach ($categoryMap as $catId => $cat) { + if ($cat['parent_id'] == $category['id']) { + $node['children'][] = $this->buildCategoryTreeWithProducts($cat, $categoryMap); + } + } + + return $node; + } + + /** + * Get category name by ID + */ + protected function getCategoryNameById($categoryId, $source = 'm1') + { + try { + $connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection; + $prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix; + + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $nameAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + if (!$nameAttributeId) { + return null; + } + + $name = DB::connection($connection) + ->table($prefix . 'catalog_category_entity_varchar') + ->where('entity_id', $categoryId) + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->value('value'); + + return $name; + } catch (Exception $e) { + Log::warning("Error getting category name for ID {$categoryId}: " . $e->getMessage()); + return null; + } + } + + /** + * Find category by name (case-insensitive, matches any parent) + */ + protected function findCategoryByName($categoryName, $source = 'm2') + { + try { + $connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection; + $prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix; + + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_category') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $nameAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'name') + ->value('attribute_id'); + + if (!$nameAttributeId) { + return null; + } + + // Find category by name (case-insensitive) + $category = DB::connection($connection) + ->table($prefix . 'catalog_category_entity_varchar') + ->where('attribute_id', $nameAttributeId) + ->where('store_id', 0) + ->whereRaw('LOWER(value) = ?', [strtolower(trim($categoryName))]) + ->first(); + + return $category ? $category->entity_id : null; + } catch (Exception $e) { + Log::warning("Error finding category by name '{$categoryName}': " . $e->getMessage()); + return null; + } + } + + /** + * Get category mapping from previous migrations + */ +protected function getCategoryMapping() + { + // Try to get mapping from category_mapping table or build it from existing categories + // Build it by matching category names (case-insensitive, ignoring parent differences) + $mapping = []; + + try { + $m1Categories = $this->getMagento1Categories(); + $m2Categories = $this->getMagento2Categories(); + + // Create a map of M2 categories by normalized name (case-insensitive) + // Use the first match if multiple categories have the same name + $m2CategoryMap = []; + foreach ($m2Categories as $m2Cat) { + $normalizedName = strtolower(trim($m2Cat->name ?? '')); + if (!empty($normalizedName) && !isset($m2CategoryMap[$normalizedName])) { + $m2CategoryMap[$normalizedName] = $m2Cat->entity_id; + } + } + + // Match M1 categories to M2 by normalized name + foreach ($m1Categories as $m1Cat) { + $normalizedName = strtolower(trim($m1Cat->name ?? '')); + if (!empty($normalizedName) && isset($m2CategoryMap[$normalizedName])) { + $mapping[$m1Cat->entity_id] = $m2CategoryMap[$normalizedName]; + } + } + } catch (Exception $e) { + Log::warning('Error building category mapping: ' . $e->getMessage()); + } + + return $mapping; + } + /** * Recursively delete all children of a category */ diff --git a/resources/views/migration/index.blade.php b/resources/views/migration/index.blade.php index ac8434f..4c30ba1 100644 --- a/resources/views/migration/index.blade.php +++ b/resources/views/migration/index.blade.php @@ -484,6 +484,127 @@ .tree-rename-cancel-btn:hover { background: #5a6268; } + + /* Delete Confirmation Popup */ + .delete-confirm-popup { + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 20px; + min-width: 320px; + max-width: 400px; + max-height: calc(100vh - 40px); + overflow-y: auto; + z-index: 10000; + border: 1px solid #e0e0e0; + animation: popupFadeIn 0.2s ease-out; + } + + .delete-confirm-popup.no-arrow::before, + .delete-confirm-popup.no-arrow::after { + display: none; + } + + @keyframes popupFadeIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + } + + .delete-confirm-popup::before { + content: ''; + position: absolute; + bottom: -8px; + left: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid white; + } + + .delete-confirm-popup::after { + content: ''; + position: absolute; + bottom: -9px; + left: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid #e0e0e0; + } + + .delete-confirm-popup h3 { + margin: 0 0 12px 0; + color: #d32f2f; + font-size: 1.1em; + display: flex; + align-items: center; + gap: 8px; + } + + .delete-confirm-popup h3::before { + content: '⚠️'; + font-size: 1.2em; + } + + .delete-confirm-popup p { + margin: 0 0 16px 0; + color: #666; + line-height: 1.5; + font-size: 0.95em; + } + + .delete-confirm-popup .popup-buttons { + display: flex; + gap: 10px; + justify-content: flex-end; + } + + .delete-confirm-popup .popup-btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + font-size: 0.9em; + cursor: pointer; + transition: all 0.2s; + font-weight: 500; + } + + .delete-confirm-popup .popup-btn-cancel { + background: #f5f5f5; + color: #333; + } + + .delete-confirm-popup .popup-btn-cancel:hover { + background: #e0e0e0; + } + + .delete-confirm-popup .popup-btn-delete { + background: #d32f2f; + color: white; + } + + .delete-confirm-popup .popup-btn-delete:hover { + background: #b71c1c; + } + + .popup-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + z-index: 9999; + }
@@ -500,6 +621,7 @@ + @@ -1001,6 +1123,322 @@ class="btn btn-primary" + + +The product migration process will:
+⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.
+Detailed logs showing which products were added, updated, or encountered errors during migration:
+View all categories with their associated products in a tree structure.
+ +This tool will update Magento 2 products to be in the same categories as their Magento 1 counterparts.
+⚠️ Warning: This will modify product-category associations in your Magento 2 database. Make sure you have a backup before proceeding.
+These products exist in Magento 1 but do not have a matching SKU or ID in Magento 2:
+ @if($m1ProductsNotInM2->count() > 0) +| ID | +SKU | +Name | +Type | +Created | +
|---|---|---|---|---|
| {{ $product->entity_id }} | +{{ $product->sku ?? 'N/A' }} | +{{ $product->name ?? 'Unnamed Product' }} | +{{ $product->type_id ?? 'N/A' }} | ++ @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif + | +
These products exist in Magento 2 but do not have a matching SKU or ID in Magento 1:
+ @if($m2ProductsNotInM1->count() > 0) +| ID | +SKU | +Name | +Type | +Created | +Actions | +
|---|---|---|---|---|---|
| {{ $product->entity_id }} | +{{ $product->sku ?? 'N/A' }} | +{{ $product->name ?? 'Unnamed Product' }} | +{{ $product->type_id ?? 'N/A' }} | ++ @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif + | ++ + | +
View all products from Magento 1 and Magento 2
+ +| ID | +SKU | +Name | +Type | +Created | +
|---|---|---|---|---|
| {{ $product->entity_id }} | +{{ $product->sku ?? 'N/A' }} | +{{ $product->name ?? 'Unnamed Product' }} | +{{ $product->type_id ?? 'N/A' }} | ++ @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif + | +
| ID | +SKU | +Name | +Type | +Created | +
|---|---|---|---|---|
| {{ $product->entity_id }} | +{{ $product->sku ?? 'N/A' }} | +{{ $product->name ?? 'Unnamed Product' }} | +{{ $product->type_id ?? 'N/A' }} | ++ @if($product->created_at) + {{ \Carbon\Carbon::parse($product->created_at)->format('Y-m-d') }} + @else + N/A + @endif + | +