diff --git a/app/Http/Controllers/ProductsController.php b/app/Http/Controllers/ProductsController.php index 5604289..c75514b 100644 --- a/app/Http/Controllers/ProductsController.php +++ b/app/Http/Controllers/ProductsController.php @@ -147,5 +147,30 @@ public function fixCategoryProducts(Request $request) ], 500); } } + + /** + * Migrate product options from M1 mageworx_custom_option_ tables to M2 mageworx_optiontemplates_ tables + */ + public function migrateProductOptions(Request $request) + { + try { + $dryRun = $request->input('dry_run', false); + $result = $this->migrationService->migrateProductOptions($dryRun); + + return response()->json($result, $result['success'] ? 200 : 400); + + } catch (\Exception $e) { + Log::error('Product options migration error: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'migrated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ], 500); + } + } } diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index ecb17f5..1f56a1b 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -1714,14 +1714,14 @@ public function getMagento1Products() // 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') + ->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'has_options', 'required_options', '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') + ->select('entity_id', 'type_id', 'attribute_set_id', 'has_options', 'required_options', 'created_at', 'updated_at') ->orderBy('entity_id') ->get(); } @@ -2081,6 +2081,16 @@ public function migrateProducts($dryRun = false) // Product exists, get its entity_id $m2ProductId = $m2Product->entity_id; if (!$dryRun) { + // Update has_options and required_options for existing products + $updateData = [ + 'has_options' => $m1Product->has_options ?? 0, + 'required_options' => $m1Product->required_options ?? 0, + 'updated_at' => now(), + ]; + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_entity') + ->where('entity_id', $m2ProductId) + ->update($updateData); $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})"; @@ -2100,6 +2110,8 @@ public function migrateProducts($dryRun = false) 'sku' => $m1Sku, 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, 'type_id' => $m1Product->type_id ?? 'simple', + 'has_options' => $m1Product->has_options ?? 0, + 'required_options' => $m1Product->required_options ?? 0, 'created_at' => $m1Product->created_at ?? now(), 'updated_at' => now(), ]); @@ -2110,6 +2122,8 @@ public function migrateProducts($dryRun = false) ->insertGetId([ 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, 'type_id' => $m1Product->type_id ?? 'simple', + 'has_options' => $m1Product->has_options ?? 0, + 'required_options' => $m1Product->required_options ?? 0, 'created_at' => $m1Product->created_at ?? now(), 'updated_at' => now(), ]); @@ -2146,6 +2160,8 @@ public function migrateProducts($dryRun = false) 'entity_id' => $m2ProductId, 'attribute_set_id' => $m1Product->attribute_set_id ?? 4, 'type_id' => $m1Product->type_id ?? 'simple', + 'has_options' => $m1Product->has_options ?? 0, + 'required_options' => $m1Product->required_options ?? 0, 'created_at' => $m1Product->created_at ?? now(), 'updated_at' => now(), ]; @@ -2253,6 +2269,15 @@ public function migrateProducts($dryRun = false) } } + // Migrate catalog_product_option tables after all products are migrated + if (!$dryRun) { + $this->migrationLog[] = "Starting catalog_product_option tables migration..."; + $optionMigrationResult = $this->migrateCatalogProductOptions(); + if ($optionMigrationResult['migrated'] > 0 || $optionMigrationResult['errors'] > 0) { + $this->migrationLog[] = "Catalog product options migration: Migrated {$optionMigrationResult['migrated']} options, Errors: {$optionMigrationResult['errors']}"; + } + } + if (!$dryRun) { DB::connection($this->magento2Connection)->commit(); } @@ -3881,6 +3906,409 @@ protected function createDefaultStockItem($productId) } } + /** + * Migrate all catalog_product_option tables from M1 to M2 + */ + protected function migrateCatalogProductOptions() + { + $migratedCount = 0; + $errorCount = 0; + + try { + // Build product ID mapping from M1 to M2 + $productIdMapping = $this->buildProductIdMapping(); + + if (empty($productIdMapping)) { + Log::warning("No product ID mapping found for catalog_product_option migration"); + return [ + 'migrated' => 0, + 'errors' => 0 + ]; + } + + // Get all M1 product options + $m1Options = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option') + ->get(); + + if ($m1Options->isEmpty()) { + return [ + 'migrated' => 0, + 'errors' => 0 + ]; + } + + // Build option_id mapping (M1 option_id => M2 option_id) + $optionIdMapping = []; + + // Migrate catalog_product_option table + foreach ($m1Options as $m1Option) { + try { + $m1ProductId = $m1Option->product_id; + $m1OptionId = $m1Option->option_id; + + // Skip if product doesn't exist in M2 + if (!isset($productIdMapping[$m1ProductId])) { + continue; + } + + $m2ProductId = $productIdMapping[$m1ProductId]; + + // Check if option already exists in M2 (by product_id and type) + $existingOption = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option') + ->where('product_id', $m2ProductId) + ->where('type', $m1Option->type) + ->where('sku', $m1Option->sku ?? '') + ->first(); + + $optionData = [ + 'product_id' => $m2ProductId, + 'type' => $m1Option->type, + 'is_require' => $m1Option->is_require ?? 0, + 'sku' => $m1Option->sku ?? null, + 'max_characters' => $m1Option->max_characters ?? null, + 'file_extension' => $m1Option->file_extension ?? null, + 'image_size_x' => $m1Option->image_size_x ?? null, + 'image_size_y' => $m1Option->image_size_y ?? null, + 'sort_order' => $m1Option->sort_order ?? 0, + ]; + + if ($existingOption) { + // Update existing option + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option') + ->where('option_id', $existingOption->option_id) + ->update($optionData); + $m2OptionId = $existingOption->option_id; + } else { + // Insert new option + $m2OptionId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option') + ->insertGetId($optionData); + } + + // Store option_id mapping + $optionIdMapping[$m1OptionId] = $m2OptionId; + $migratedCount++; + + } catch (Exception $e) { + $errorCount++; + Log::error("Error migrating product option M1 ID {$m1OptionId}: " . $e->getMessage()); + } + } + + // Migrate catalog_product_option_price + $this->migrateCatalogProductOptionPrice($optionIdMapping); + + // Migrate catalog_product_option_title + $this->migrateCatalogProductOptionTitle($optionIdMapping); + + // Migrate catalog_product_option_type_value + $typeValueMapping = $this->migrateCatalogProductOptionTypeValue($optionIdMapping); + + // Migrate catalog_product_option_type_price + $this->migrateCatalogProductOptionTypePrice($typeValueMapping); + + // Migrate catalog_product_option_type_title + $this->migrateCatalogProductOptionTypeTitle($typeValueMapping); + + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option tables: " . $e->getMessage()); + $errorCount++; + } + + return [ + 'migrated' => $migratedCount, + 'errors' => $errorCount + ]; + } + + /** + * Build product ID mapping from M1 to M2 (by SKU or entity_id) + */ + protected function buildProductIdMapping() + { + $mapping = []; + + try { + $m1Products = $this->getMagento1Products(); + $m2Products = $this->getMagento2Products(); + + // Build M2 product lookup by SKU and entity_id + $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; + } + + // Build mapping from M1 to M2 + foreach ($m1Products as $m1Product) { + $m1ProductId = $m1Product->entity_id; + $m1Sku = $m1Product->sku ?? 'N/A'; + + $m2ProductId = null; + + if ($m1Sku !== 'N/A' && !empty($m1Sku)) { + // Match by SKU + if (isset($m2ProductBySku[$m1Sku])) { + $m2ProductId = $m2ProductBySku[$m1Sku]; + } + } else { + // Match by entity_id + if (isset($m2ProductById[$m1ProductId])) { + $m2ProductId = $m1ProductId; + } + } + + if ($m2ProductId) { + $mapping[$m1ProductId] = $m2ProductId; + } + } + + } catch (Exception $e) { + Log::error("Error building product ID mapping: " . $e->getMessage()); + } + + return $mapping; + } + + /** + * Migrate catalog_product_option_price + */ + protected function migrateCatalogProductOptionPrice($optionIdMapping) + { + try { + foreach ($optionIdMapping as $m1OptionId => $m2OptionId) { + $m1Prices = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option_price') + ->where('option_id', $m1OptionId) + ->get(); + + foreach ($m1Prices as $m1Price) { + $priceData = [ + 'option_id' => $m2OptionId, + 'store_id' => $m1Price->store_id ?? 0, + 'price' => $m1Price->price ?? 0, + 'price_type' => $m1Price->price_type ?? 'fixed', + ]; + + // Check if exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_price') + ->where('option_id', $m2OptionId) + ->where('store_id', $priceData['store_id']) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_price') + ->where('option_id', $m2OptionId) + ->where('store_id', $priceData['store_id']) + ->update($priceData); + } else { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_price') + ->insert($priceData); + } + } + } + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option_price: " . $e->getMessage()); + } + } + + /** + * Migrate catalog_product_option_title + */ + protected function migrateCatalogProductOptionTitle($optionIdMapping) + { + try { + foreach ($optionIdMapping as $m1OptionId => $m2OptionId) { + $m1Titles = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option_title') + ->where('option_id', $m1OptionId) + ->get(); + + foreach ($m1Titles as $m1Title) { + $titleData = [ + 'option_id' => $m2OptionId, + 'store_id' => $m1Title->store_id ?? 0, + 'title' => $m1Title->title ?? '', + ]; + + // Check if exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_title') + ->where('option_id', $m2OptionId) + ->where('store_id', $titleData['store_id']) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_title') + ->where('option_id', $m2OptionId) + ->where('store_id', $titleData['store_id']) + ->update($titleData); + } else { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_title') + ->insert($titleData); + } + } + } + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option_title: " . $e->getMessage()); + } + } + + /** + * Migrate catalog_product_option_type_value and return type_value_id mapping + */ + protected function migrateCatalogProductOptionTypeValue($optionIdMapping) + { + $typeValueMapping = []; + + try { + foreach ($optionIdMapping as $m1OptionId => $m2OptionId) { + $m1TypeValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option_type_value') + ->where('option_id', $m1OptionId) + ->get(); + + foreach ($m1TypeValues as $m1TypeValue) { + $m1TypeValueId = $m1TypeValue->option_type_id; + + $typeValueData = [ + 'option_id' => $m2OptionId, + 'sku' => $m1TypeValue->sku ?? null, + 'sort_order' => $m1TypeValue->sort_order ?? 0, + ]; + + // Check if exists (by option_id and sku/sort_order) + $existing = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_value') + ->where('option_id', $m2OptionId) + ->where('sku', $typeValueData['sku']) + ->where('sort_order', $typeValueData['sort_order']) + ->first(); + + if ($existing) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_value') + ->where('option_type_id', $existing->option_type_id) + ->update($typeValueData); + $m2TypeValueId = $existing->option_type_id; + } else { + $m2TypeValueId = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_value') + ->insertGetId($typeValueData); + } + + $typeValueMapping[$m1TypeValueId] = $m2TypeValueId; + } + } + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option_type_value: " . $e->getMessage()); + } + + return $typeValueMapping; + } + + /** + * Migrate catalog_product_option_type_price + */ + protected function migrateCatalogProductOptionTypePrice($typeValueMapping) + { + try { + foreach ($typeValueMapping as $m1TypeValueId => $m2TypeValueId) { + $m1Prices = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option_type_price') + ->where('option_type_id', $m1TypeValueId) + ->get(); + + foreach ($m1Prices as $m1Price) { + $priceData = [ + 'option_type_id' => $m2TypeValueId, + 'store_id' => $m1Price->store_id ?? 0, + 'price' => $m1Price->price ?? 0, + 'price_type' => $m1Price->price_type ?? 'fixed', + ]; + + // Check if exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_price') + ->where('option_type_id', $m2TypeValueId) + ->where('store_id', $priceData['store_id']) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_price') + ->where('option_type_id', $m2TypeValueId) + ->where('store_id', $priceData['store_id']) + ->update($priceData); + } else { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_price') + ->insert($priceData); + } + } + } + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option_type_price: " . $e->getMessage()); + } + } + + /** + * Migrate catalog_product_option_type_title + */ + protected function migrateCatalogProductOptionTypeTitle($typeValueMapping) + { + try { + foreach ($typeValueMapping as $m1TypeValueId => $m2TypeValueId) { + $m1Titles = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_option_type_title') + ->where('option_type_id', $m1TypeValueId) + ->get(); + + foreach ($m1Titles as $m1Title) { + $titleData = [ + 'option_type_id' => $m2TypeValueId, + 'store_id' => $m1Title->store_id ?? 0, + 'title' => $m1Title->title ?? '', + ]; + + // Check if exists + $exists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_title') + ->where('option_type_id', $m2TypeValueId) + ->where('store_id', $titleData['store_id']) + ->exists(); + + if ($exists) { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_title') + ->where('option_type_id', $m2TypeValueId) + ->where('store_id', $titleData['store_id']) + ->update($titleData); + } else { + DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'catalog_product_option_type_title') + ->insert($titleData); + } + } + } + } catch (Exception $e) { + Log::error("Error migrating catalog_product_option_type_title: " . $e->getMessage()); + } + } + /** * Ensure product is enabled and visible (required for products to appear in categories after reindex) */ @@ -5171,5 +5599,607 @@ public function deleteM2Customer($customerId) ]; } } + + /** + * Migrate product options from M1 mageworx_custom_option_ tables to M2 mageworx_optiontemplates_ tables + */ + public function migrateProductOptions($dryRun = false) + { + $this->migrationLog = []; + $migratedCount = 0; + $skippedCount = 0; + $errorCount = 0; + + try { + // Get all mageworx custom option tables from M1 + $m1Tables = $this->getMageworxM1Tables(); + if (empty($m1Tables)) { + return [ + 'success' => false, + 'message' => 'No mageworx_custom_option_ tables found in Magento 1', + 'migrated' => 0, + 'skipped' => 0, + 'errors' => 0, + 'log' => [] + ]; + } + + $this->migrationLog[] = "Found " . count($m1Tables) . " mageworx custom option tables in M1"; + + // Get available M2 tables for reference + $m2Tables = $this->getMageworxM2Tables(); + $this->migrationLog[] = "Found " . count($m2Tables) . " mageworx option templates tables in M2"; + + // Map M1 table names to M2 table names + $tableMapping = $this->getMageworxTableMapping(); + + // Sort tables by dependency order (parent tables first) + $sortedTables = $this->sortTablesByDependency($m1Tables, $tableMapping); + $this->migrationLog[] = "Migrating tables in dependency order"; + + foreach ($sortedTables as $m1Table) { + try { + // Get the corresponding M2 table name + $m2Table = $this->getM2TableName($m1Table, $tableMapping); + if (!$m2Table) { + $this->migrationLog[] = "INFO: Skipping M1 table {$m1Table} - no corresponding M2 table exists or mapping not configured"; + $skippedCount++; + continue; + } + + // Check if M2 table exists + if (!$this->tableExists($this->magento2Connection, $this->magento2Prefix . $m2Table)) { + $this->migrationLog[] = "WARNING: M2 table does not exist: {$m2Table} (mapped from M1: {$m1Table}). Available M2 tables: " . implode(', ', array_slice($m2Tables, 0, 10)); + $skippedCount++; + continue; + } + + // Get all data from M1 table + $m1Data = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . $m1Table) + ->get(); + + if ($m1Data->isEmpty()) { + $this->migrationLog[] = "INFO: No data in M1 table: {$m1Table}"; + continue; + } + + $this->migrationLog[] = "Migrating {$m1Data->count()} records from {$m1Table} to {$m2Table}"; + + if (!$dryRun) { + DB::connection($this->magento2Connection)->beginTransaction(); + + try { + foreach ($m1Data as $row) { + $rowArray = (array) $row; + + // Map column names if needed + $mappedRow = $this->mapMageworxColumns($m1Table, $m2Table, $rowArray); + + // Validate foreign key constraints + $fkValid = $this->validateForeignKeyConstraints($m2Table, $mappedRow); + if (!$fkValid) { + $skippedCount++; + continue; + } + + // Check if record already exists in M2 (by primary key or unique identifier) + $exists = $this->recordExists($m2Table, $mappedRow); + + if ($exists) { + // Update existing record + $this->updateM2Record($m2Table, $mappedRow); + $this->migrationLog[] = "Updated record in {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")"; + } else { + // Insert new record + $this->insertM2Record($m2Table, $mappedRow); + $this->migrationLog[] = "Inserted record into {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")"; + } + + $migratedCount++; + } + + DB::connection($this->magento2Connection)->commit(); + } catch (Exception $e) { + DB::connection($this->magento2Connection)->rollBack(); + throw $e; + } + } else { + // Dry run - just log what would be migrated + foreach ($m1Data as $row) { + $rowArray = (array) $row; + $mappedRow = $this->mapMageworxColumns($m1Table, $m2Table, $rowArray); + + // Validate foreign key constraints in dry run + $fkValid = $this->validateForeignKeyConstraints($m2Table, $mappedRow, true); + if (!$fkValid) { + $skippedCount++; + continue; + } + + $exists = $this->recordExists($m2Table, $mappedRow); + + if ($exists) { + $this->migrationLog[] = "Would update record in {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")"; + } else { + $this->migrationLog[] = "Would insert record into {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")"; + } + + $migratedCount++; + } + } + + } catch (Exception $e) { + $errorCount++; + $this->migrationLog[] = "ERROR migrating {$m1Table}: " . $e->getMessage(); + Log::error("Error migrating mageworx table {$m1Table}: " . $e->getMessage()); + } + } + + return [ + 'success' => true, + 'message' => $dryRun ? 'Dry run completed' : 'Product options migration completed', + 'migrated' => $migratedCount, + 'skipped' => $skippedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + + } catch (Exception $e) { + Log::error('Product options migration error: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'Migration failed: ' . $e->getMessage(), + 'migrated' => $migratedCount, + 'skipped' => $skippedCount, + 'errors' => $errorCount, + 'log' => $this->migrationLog + ]; + } + } + + /** + * Get all mageworx custom option tables from M1 + */ + protected function getMageworxM1Tables() + { + try { + $tables = []; + $database = config("database.connections.{$this->magento1Connection}.database"); + $prefix = $this->magento1Prefix; + + // Get all tables from information_schema - check for both singular and plural forms + $allTables = DB::connection($this->magento1Connection) + ->select("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND (TABLE_NAME LIKE ? OR TABLE_NAME LIKE ?)", [ + $database, + $prefix . 'mageworx_custom_option_%', + $prefix . 'mageworx_custom_options_%' + ]); + + foreach ($allTables as $table) { + $tableName = $table->TABLE_NAME; + // Remove prefix to get base table name + $baseTableName = str_replace($prefix, '', $tableName); + $tables[] = $baseTableName; + } + + return $tables; + } catch (Exception $e) { + Log::error('Error getting mageworx M1 tables: ' . $e->getMessage()); + return []; + } + } + + /** + * Get all mageworx option templates tables from M2 + */ + protected function getMageworxM2Tables() + { + try { + $tables = []; + $database = config("database.connections.{$this->magento2Connection}.database"); + $prefix = $this->magento2Prefix; + + // Get all tables from information_schema + $allTables = DB::connection($this->magento2Connection) + ->select("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?", [ + $database, + $prefix . 'mageworx_optiontemplates_%' + ]); + + foreach ($allTables as $table) { + $tableName = $table->TABLE_NAME; + // Remove prefix to get base table name + $baseTableName = str_replace($prefix, '', $tableName); + $tables[] = $baseTableName; + } + + return $tables; + } catch (Exception $e) { + Log::error('Error getting mageworx M2 tables: ' . $e->getMessage()); + return []; + } + } + + /** + * Get table name mapping from M1 to M2 + */ + protected function getMageworxTableMapping() + { + return [ + // Singular form mappings + 'mageworx_custom_option_template' => 'mageworx_optiontemplates_group', + 'mageworx_custom_option_group' => 'mageworx_optiontemplates_group', + 'mageworx_custom_option_option' => 'mageworx_optiontemplates_group_option', + 'mageworx_custom_option_value' => 'mageworx_optiontemplates_option_type_value', + 'mageworx_custom_option_type' => 'mageworx_optiontemplates_option_type', + 'mageworx_custom_option_store' => 'mageworx_optiontemplates_store', + + // Plural form mappings (actual M1 table names) + // Note: M2 uses "group_option_" prefix structure, not just "option_" + 'mageworx_custom_options_group' => 'mageworx_optiontemplates_group', + 'mageworx_custom_options_group_store' => 'mageworx_optiontemplates_group_option_store_view', // M2 uses store_view + 'mageworx_custom_options_option_default' => null, // May not exist in M2 or merged into another table + 'mageworx_custom_options_option_description' => 'mageworx_optiontemplates_group_option_description', + 'mageworx_custom_options_option_type_description' => 'mageworx_optiontemplates_group_option_type_description', + 'mageworx_custom_options_option_type_image' => 'mageworx_optiontemplates_group_option_type_image', + 'mageworx_custom_options_option_type_special_price' => null, // May not exist in M2 or merged into price table + 'mageworx_custom_options_option_type_tier_price' => null, // May not exist in M2 or merged into price table + 'mageworx_custom_options_option_view_mode' => null, // May not exist in M2 + 'mageworx_custom_options_relation' => 'mageworx_optiontemplates_relation', // M2 table exists + ]; + } + + /** + * Get M2 table name for M1 table + */ + protected function getM2TableName($m1Table, $tableMapping) + { + // Direct mapping (including null values for tables that don't exist in M2) + if (isset($tableMapping[$m1Table])) { + return $tableMapping[$m1Table]; + } + + // Try pattern matching for singular form (e.g., mageworx_custom_option_* -> mageworx_optiontemplates_*) + if (strpos($m1Table, 'mageworx_custom_option_') === 0) { + $suffix = str_replace('mageworx_custom_option_', '', $m1Table); + + // M2 uses "group_option_" structure for option-related tables + if (strpos($suffix, 'option_') === 0) { + return 'mageworx_optiontemplates_group_' . $suffix; + } + + return 'mageworx_optiontemplates_' . $suffix; + } + + // Try pattern matching for plural form (e.g., mageworx_custom_options_* -> mageworx_optiontemplates_*) + if (strpos($m1Table, 'mageworx_custom_options_') === 0) { + // Explicitly skip view_mode table + if ($m1Table === 'mageworx_custom_options_option_view_mode') { + return null; + } + + // Explicitly skip option_default table + if ($m1Table === 'mageworx_custom_options_option_default') { + return null; + } + + $suffix = str_replace('mageworx_custom_options_', '', $m1Table); + + // Handle special cases for M2 structure + // M2 uses "group_option_" prefix for option-related tables + if (strpos($suffix, 'option_') === 0) { + return 'mageworx_optiontemplates_group_' . $suffix; + } + + // Handle group_store -> group_option_store_view + if ($suffix === 'group_store') { + return 'mageworx_optiontemplates_group_option_store_view'; + } + + // Default: just replace prefix + return 'mageworx_optiontemplates_' . $suffix; + } + + return null; + } + + /** + * Check if table exists + */ + protected function tableExists($connection, $tableName) + { + try { + DB::connection($connection) + ->table($tableName) + ->limit(1) + ->first(); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Map column names from M1 to M2 if needed + */ + protected function mapMageworxColumns($m1Table, $m2Table, $rowArray) + { + // Table-specific column mappings + $tableColumnMappings = [ + 'mageworx_optiontemplates_group_option_type_image' => [ + 'image_file' => 'image', // M2 uses 'image' instead of 'image_file' + ], + ]; + + // Common column mappings + $columnMappings = [ + // Generic mappings that might apply + 'custom_option_id' => 'option_id', + 'custom_option_group_id' => 'group_id', + ]; + + // Get table-specific mappings if they exist + $tableMappings = $tableColumnMappings[$m2Table] ?? []; + + $mappedRow = []; + foreach ($rowArray as $key => $value) { + // First check table-specific mapping, then common mapping, then use original key + if (isset($tableMappings[$key])) { + $mappedKey = $tableMappings[$key]; + } elseif (isset($columnMappings[$key])) { + $mappedKey = $columnMappings[$key]; + } else { + $mappedKey = $key; + } + $mappedRow[$mappedKey] = $value; + } + + return $mappedRow; + } + + /** + * Check if record exists in M2 table + */ + protected function recordExists($m2Table, $rowArray) + { + try { + $table = $this->magento2Prefix . $m2Table; + + // Try common primary key columns in order of preference + $primaryKeys = ['id', 'option_id', 'group_id', 'value_id', 'type_id', 'store_id']; + + foreach ($primaryKeys as $key) { + if (isset($rowArray[$key]) && $rowArray[$key] !== null) { + $exists = DB::connection($this->magento2Connection) + ->table($table) + ->where($key, $rowArray[$key]) + ->exists(); + + if ($exists) { + return true; + } + } + } + + return false; + } catch (Exception $e) { + Log::warning("Error checking if record exists in {$m2Table}: " . $e->getMessage()); + return false; + } + } + + /** + * Sort tables by dependency order (parent tables first) + */ + protected function sortTablesByDependency($m1Tables, $tableMapping) + { + // Define table dependency order (parent tables first) + $dependencyOrder = [ + 'mageworx_optiontemplates_group', // Parent - no dependencies + 'mageworx_optiontemplates_group_option', // Depends on group + 'mageworx_optiontemplates_group_option_type_value', // Depends on group_option + 'mageworx_optiontemplates_group_option_description', // Depends on group_option + 'mageworx_optiontemplates_group_option_title', // Depends on group_option + 'mageworx_optiontemplates_group_option_price', // Depends on group_option + 'mageworx_optiontemplates_group_option_type_description', // Depends on group_option_type_value + 'mageworx_optiontemplates_group_option_type_image', // Depends on group_option_type_value + 'mageworx_optiontemplates_group_option_store_view', // Depends on group_option + 'mageworx_optiontemplates_relation', // Depends on group + ]; + + $sorted = []; + $unsorted = []; + + // First, add tables in dependency order + foreach ($dependencyOrder as $m2TableName) { + // Find M1 table that maps to this M2 table + foreach ($m1Tables as $m1Table) { + $m2Table = $this->getM2TableName($m1Table, $tableMapping); + if ($m2Table === $m2TableName) { + $sorted[] = $m1Table; + break; + } + } + } + + // Add any remaining tables that weren't in the dependency list + foreach ($m1Tables as $m1Table) { + if (!in_array($m1Table, $sorted)) { + $sorted[] = $m1Table; + } + } + + return $sorted; + } + + /** + * Validate foreign key constraints before inserting/updating + */ + protected function validateForeignKeyConstraints($m2Table, $mappedRow, $dryRun = false) + { + $prefix = $dryRun ? "Would skip" : "Skipping"; + + // Validate option_id foreign key for option_description + if ($m2Table === 'mageworx_optiontemplates_group_option_description' && isset($mappedRow['option_id'])) { + $optionExists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'mageworx_optiontemplates_group_option') + ->where('option_id', $mappedRow['option_id']) + ->exists(); + + if (!$optionExists) { + $this->migrationLog[] = "WARNING: {$prefix} option_description record - option_id {$mappedRow['option_id']} does not exist in mageworx_optiontemplates_group_option"; + return false; + } + } + + // Validate option_type_id foreign key for option_type_image + if ($m2Table === 'mageworx_optiontemplates_group_option_type_image' && isset($mappedRow['option_type_id'])) { + $typeExists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'mageworx_optiontemplates_group_option_type_value') + ->where('option_type_id', $mappedRow['option_type_id']) + ->exists(); + + if (!$typeExists) { + $this->migrationLog[] = "WARNING: {$prefix} option_type_image record - option_type_id {$mappedRow['option_type_id']} does not exist in mageworx_optiontemplates_group_option_type_value"; + return false; + } + } + + // Validate group_id foreign key for relation table + if ($m2Table === 'mageworx_optiontemplates_relation' && isset($mappedRow['group_id'])) { + $groupExists = DB::connection($this->magento2Connection) + ->table($this->magento2Prefix . 'mageworx_optiontemplates_group') + ->where('group_id', $mappedRow['group_id']) + ->exists(); + + if (!$groupExists) { + $this->migrationLog[] = "WARNING: {$prefix} relation record - group_id {$mappedRow['group_id']} does not exist in mageworx_optiontemplates_group"; + return false; + } + } + + return true; + } + + /** + * Get columns that exist in M2 table + */ + protected function getM2TableColumns($m2Table) + { + static $columnCache = []; + + if (isset($columnCache[$m2Table])) { + return $columnCache[$m2Table]; + } + + try { + $database = config("database.connections.{$this->magento2Connection}.database"); + $prefix = $this->magento2Prefix; + + $columns = DB::connection($this->magento2Connection) + ->select("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", [ + $database, + $prefix . $m2Table + ]); + + $columnNames = array_map(function($col) { + return $col->COLUMN_NAME; + }, $columns); + + $columnCache[$m2Table] = $columnNames; + return $columnNames; + } catch (Exception $e) { + Log::warning("Error getting columns for {$m2Table}: " . $e->getMessage()); + return []; + } + } + + /** + * Insert record into M2 table + */ + protected function insertM2Record($m2Table, $rowArray) + { + $table = $this->magento2Prefix . $m2Table; + + // Get valid columns for this table + $validColumns = $this->getM2TableColumns($m2Table); + + // Filter row to only include columns that exist in M2 table + $filteredRow = []; + foreach ($rowArray as $key => $value) { + if (in_array($key, $validColumns)) { + $filteredRow[$key] = $value; + } else { + $this->migrationLog[] = "INFO: Skipping column '{$key}' - does not exist in M2 table {$m2Table}"; + } + } + + // Remove null values + $cleanRow = array_filter($filteredRow, function($value) { + return $value !== null; + }); + + if (empty($cleanRow)) { + $this->migrationLog[] = "WARNING: No valid columns to insert for record in {$m2Table}"; + return; + } + + DB::connection($this->magento2Connection) + ->table($table) + ->insert($cleanRow); + } + + /** + * Update record in M2 table + */ + protected function updateM2Record($m2Table, $rowArray) + { + $table = $this->magento2Prefix . $m2Table; + + // Find primary key + $primaryKeys = ['id', 'option_id', 'group_id', 'value_id', 'type_id', 'store_id']; + $whereClause = []; + + foreach ($primaryKeys as $key) { + if (isset($rowArray[$key]) && $rowArray[$key] !== null) { + $whereClause[$key] = $rowArray[$key]; + break; + } + } + + if (empty($whereClause)) { + throw new Exception("Cannot update record: no primary key found in table {$m2Table}"); + } + + // Remove primary key from update data + $updateData = $rowArray; + foreach ($primaryKeys as $key) { + unset($updateData[$key]); + } + + // Get valid columns for this table + $validColumns = $this->getM2TableColumns($m2Table); + + // Filter update data to only include columns that exist in M2 table + $filteredData = []; + foreach ($updateData as $key => $value) { + if (in_array($key, $validColumns)) { + $filteredData[$key] = $value; + } + } + + // Remove null values + $cleanData = array_filter($filteredData, function($value) { + return $value !== null; + }); + + if (!empty($cleanData)) { + DB::connection($this->magento2Connection) + ->table($table) + ->where($whereClause) + ->update($cleanData); + } + } } diff --git a/resources/js/products.js b/resources/js/products.js index 30b1db2..cf62763 100644 --- a/resources/js/products.js +++ b/resources/js/products.js @@ -452,8 +452,103 @@ function createCategoryWithProductsNode(node) { return nodeDiv; } +function startProductOptionsMigration(dryRun) { + // Check if routes are available + if (!routes || !routes.migrateProductOptions) { + console.error('Product options routes not available', routes); + alert('Error: Routes not initialized. Please refresh the page.'); + return; + } + + const button = dryRun ? document.getElementById('dryRunProductOptionsMigrationBtn') : document.getElementById('startProductOptionsMigrationBtn'); + const otherButton = dryRun ? document.getElementById('startProductOptionsMigrationBtn') : document.getElementById('dryRunProductOptionsMigrationBtn'); + const logContainer = document.getElementById('productOptionsMigrationLogContainer'); + const logContent = document.getElementById('productOptionsMigrationLogContent'); + + if (!button) { + console.error('Button not found'); + alert('Error: Button not found. Please refresh the page.'); + return; + } + + const originalText = button.textContent; + button.disabled = true; + if (otherButton) { + otherButton.disabled = true; + } + button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...'; + button.style.cursor = 'not-allowed'; + + logContainer.style.display = 'block'; + logContent.innerHTML = '
catalog_product_option tables will be migrated, including options, prices, titles, and type values⚠️ Warning: This will modify your Magento 2 database. Make sure you have a backup before proceeding.
@@ -30,6 +31,52 @@ + +Detailed logs showing which products were added, updated, or encountered errors during migration:
+The product options migration process will:
+mageworx_custom_option_* tables in Magento 1mageworx_optiontemplates_* tables in Magento 2⚠️ 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:
-