updated customers

This commit is contained in:
Chris Rosenau 2025-12-11 23:07:37 -07:00
parent 7777b6e9e7
commit 353f9e0564
8 changed files with 369 additions and 40 deletions

View File

@ -5,6 +5,7 @@
use App\Services\MagentoCategoryMigrationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class CustomersController extends Controller
{
@ -39,13 +40,30 @@ public function index()
public function migrateCustomers(Request $request)
{
try {
// Handle both JSON and form data
$dryRun = false;
$progressKey = null;
if ($request->isJson()) {
$dryRun = $request->json()->get('dry_run', false);
$progressKey = $request->json()->get('progress_key', null);
} else {
$dryRun = $request->input('dry_run', false);
$result = $this->migrationService->migrateCustomers($dryRun);
$progressKey = $request->input('progress_key', null);
}
// Convert string "true"/"false" to boolean if needed
if (is_string($dryRun)) {
$dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOLEAN);
}
$result = $this->migrationService->migrateCustomers($dryRun, $progressKey);
return response()->json($result, $result['success'] ? 200 : 400);
} catch (\Exception $e) {
Log::error('Customer migration error: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return response()->json([
'success' => false,
@ -58,6 +76,46 @@ public function migrateCustomers(Request $request)
}
}
/**
* Get customer migration progress
*/
public function getMigrationProgress(Request $request)
{
try {
$progressKey = $request->input('progress_key');
if (empty($progressKey)) {
return response()->json([
'success' => false,
'message' => 'Progress key is required'
], 400);
}
$progress = Cache::get($progressKey);
if (!$progress) {
return response()->json([
'success' => false,
'message' => 'Progress not found',
'progress' => null
], 404);
}
return response()->json([
'success' => true,
'progress' => $progress
]);
} catch (\Exception $e) {
Log::error('Get customer migration progress error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to get progress: ' . $e->getMessage()
], 500);
}
}
/**
* Delete a single customer from Magento 2
*/

View File

@ -4,6 +4,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Exception;
class MagentoCategoryMigrationService
@ -5678,7 +5679,7 @@ public function getM2CustomersNotInM1()
/**
* Migrate all customers from Magento 1 to Magento 2
*/
public function migrateCustomers($dryRun = false)
public function migrateCustomers($dryRun = false, $progressKey = null)
{
try {
$this->migrationLog = [];
@ -5710,6 +5711,20 @@ public function migrateCustomers($dryRun = false)
// Get all M1 customers
$m1Customers = $this->getMagento1Customers();
$totalCustomers = $m1Customers->count();
// Initialize progress tracking
if ($progressKey && !$dryRun) {
Cache::put($progressKey, [
'total' => $totalCustomers,
'current' => 0,
'added' => 0,
'updated' => 0,
'errors' => 0,
'status' => 'running',
'current_email' => ''
], 3600);
}
// Get all customer attribute IDs from M1
$m1AttributeIds = DB::connection($this->magento1Connection)
@ -5729,10 +5744,25 @@ public function migrateCustomers($dryRun = false)
DB::connection($this->magento2Connection)->beginTransaction();
}
$currentIndex = 0;
foreach ($m1Customers as $m1Customer) {
$currentIndex++;
try {
$m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null;
// Update progress if tracking enabled
if ($progressKey && !$dryRun) {
Cache::put($progressKey, [
'total' => $totalCustomers,
'current' => $currentIndex,
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'status' => 'running',
'current_email' => $m1Email ?? 'N/A'
], 3600);
}
if (empty($m1Email)) {
$this->migrationLog[] = "SKIPPED: Customer ID {$m1Customer->entity_id} - no email address";
continue;
@ -5845,6 +5875,19 @@ public function migrateCustomers($dryRun = false)
DB::connection($this->magento2Connection)->commit();
}
// Update progress to completed
if ($progressKey && !$dryRun) {
Cache::put($progressKey, [
'total' => $totalCustomers,
'current' => $totalCustomers,
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'status' => 'completed',
'current_email' => ''
], 3600);
}
return [
'success' => true,
'message' => $dryRun ? 'Dry run completed' : 'Customer migration completed',
@ -5858,6 +5901,20 @@ public function migrateCustomers($dryRun = false)
if (!$dryRun) {
DB::connection($this->magento2Connection)->rollBack();
}
// Update progress to failed
if ($progressKey && !$dryRun) {
Cache::put($progressKey, [
'total' => isset($totalCustomers) ? $totalCustomers : 0,
'current' => isset($currentIndex) ? $currentIndex : 0,
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'status' => 'failed',
'current_email' => ''
], 3600);
}
Log::error('Error migrating customers: ' . $e->getMessage());
return [
'success' => false,

View File

@ -2744,13 +2744,40 @@ protected function migrateCatalogProductOptions()
$m2ProductId = $productIdMapping[$m1ProductId];
// Check if option already exists in M2 (by product_id and type)
// Check if option already exists in M2
// Match by product_id, type, sort_order, and sku (if sku is not null/empty)
// This ensures we find the exact same option and update it instead of creating duplicates
$m1Sku = $m1Option->sku ?? null;
$m1SortOrder = $m1Option->sort_order ?? 0;
$query = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option')
->where('product_id', $m2ProductId)
->where('type', $m1Option->type)
->where('sort_order', $m1SortOrder);
// If SKU is provided and not empty, include it in the match
if (!empty($m1Sku)) {
$query->where('sku', $m1Sku);
} else {
// If SKU is null/empty, match options where SKU is also null/empty
$query->where(function($q) {
$q->whereNull('sku')->orWhere('sku', '');
});
}
$existingOption = $query->first();
// If still no match, try matching by product_id, type, and sort_order only
// This catches cases where SKU might differ but it's the same option
if (!$existingOption) {
$existingOption = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option')
->where('product_id', $m2ProductId)
->where('type', $m1Option->type)
->where('sku', $m1Option->sku ?? '')
->where('sort_order', $m1SortOrder)
->first();
}
$optionData = [
'product_id' => $m2ProductId,

View File

@ -9,39 +9,115 @@ document.addEventListener('DOMContentLoaded', function() {
routes = window.customerRoutes;
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
}
// Attach event listener to the button instead of using inline onclick
const startButton = document.getElementById('startCustomerMigrationBtn');
if (startButton) {
startButton.addEventListener('click', function(e) {
e.preventDefault();
startCustomerMigration();
});
}
});
function startCustomerMigration(dryRun) {
const button = dryRun ? document.getElementById('dryRunCustomerMigrationBtn') : document.getElementById('startCustomerMigrationBtn');
const otherButton = dryRun ? document.getElementById('startCustomerMigrationBtn') : document.getElementById('dryRunCustomerMigrationBtn');
const originalText = button.textContent;
button.disabled = true;
otherButton.disabled = true;
button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...';
button.style.cursor = 'not-allowed';
function startCustomerMigration() {
try {
const button = document.getElementById('startCustomerMigrationBtn');
if (!button) {
console.error('Start customer migration button not found');
alert('Error: Button not found. Please refresh the page.');
return;
}
const logContent = document.getElementById('customerMigrationLogContent');
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
if (!logContent) {
console.error('Customer migration log content not found');
alert('Error: Log container not found. Please refresh the page.');
return;
}
const originalText = button.textContent || 'Start Customer Migration';
if (button) {
button.disabled = true;
button.textContent = 'Migrating...';
button.style.cursor = 'not-allowed';
}
const progressContainer = document.getElementById('migrationProgressContainer');
// Show progress bar
if (progressContainer) {
progressContainer.style.display = 'block';
updateProgressBar(0, 0, 0, 0, 0, 'Starting migration...', '');
}
logContent.innerHTML = '<div class="log-entry">Starting migration...</div>';
// Generate progress key for tracking
const progressKey = 'customer_migration_progress_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
let progressInterval = null;
// Check if routes are available
if (!routes || !routes.migrateCustomers) {
console.error('Customer routes not available', routes);
if (button) {
button.disabled = false;
button.textContent = originalText;
button.style.cursor = 'pointer';
}
alert('Error: Routes not initialized. Please refresh the page.');
return;
}
progressInterval = pollMigrationProgress(progressKey, button, originalText, logContent);
fetch(routes.migrateCustomers, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: JSON.stringify({ dry_run: dryRun })
body: JSON.stringify({ dry_run: false, progress_key: progressKey })
})
.then(async response => {
const contentType = response.headers.get('content-type');
// Check if response is JSON
if (contentType && contentType.includes('application/json')) {
return response.json();
} else {
// Response is HTML (error page), get text and show error
const text = await response.text();
throw new Error(`Server returned HTML instead of JSON. Status: ${response.status}. This usually means there was a server error. Check the browser console or server logs for details.`);
}
})
.then(response => response.json())
.then(data => {
// Stop polling
if (progressInterval) {
clearInterval(progressInterval);
}
// Hide progress bar
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (button) {
button.disabled = false;
otherButton.disabled = false;
button.textContent = originalText;
button.style.cursor = 'pointer';
}
if (data.success) {
// Update progress bar to 100% before hiding
if (progressContainer) {
updateProgressBar(100, data.added + data.updated, data.added + data.updated, data.added, data.updated, data.errors, 'completed', '');
}
const successEntry = document.createElement('div');
successEntry.className = 'log-entry success';
successEntry.textContent = `${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`;
successEntry.textContent = `Migration completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`;
logContent.appendChild(successEntry);
if (data.log && data.log.length > 0) {
@ -58,21 +134,116 @@ function startCustomerMigration(dryRun) {
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
errorEntry.textContent = '✗ Migration failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
}
})
.catch(error => {
// Stop polling
if (progressInterval) {
clearInterval(progressInterval);
}
// Hide progress bar
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (button) {
button.disabled = false;
otherButton.disabled = false;
button.textContent = originalText;
button.style.cursor = 'pointer';
}
if (logContent) {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Error: ' + error.message;
logContent.appendChild(errorEntry);
} else {
console.error('Error during migration:', error);
alert('Error: ' + error.message);
}
});
} catch (error) {
console.error('Error in startCustomerMigration:', error);
alert('An unexpected error occurred. Please refresh the page and try again.');
}
}
function pollMigrationProgress(progressKey, button, originalText, logContent) {
return setInterval(() => {
if (!routes.migrationProgress) {
console.error('Migration progress route not available');
return;
}
fetch(routes.migrationProgress + '?progress_key=' + encodeURIComponent(progressKey), {
method: 'GET',
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.progress) {
const progress = data.progress;
const percentage = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
updateProgressBar(
percentage,
progress.current,
progress.total,
progress.added,
progress.updated,
progress.errors,
progress.status,
progress.current_email
);
} else if (data.success === false && data.message === 'Progress not found') {
// Progress not found yet, migration might not have started
// This is okay, just continue polling
}
})
.catch(error => {
console.error('Error polling progress:', error);
});
}, 500); // Poll every 500ms for smoother updates
}
function updateProgressBar(percentage, current, total, added, updated, errors, status, currentEmail) {
const progressBar = document.getElementById('progressBar');
const progressPercentage = document.getElementById('progressPercentage');
const progressStatus = document.getElementById('progressStatus');
const currentCustomer = document.getElementById('currentCustomer');
const progressAdded = document.getElementById('progressAdded');
const progressUpdated = document.getElementById('progressUpdated');
const progressErrors = document.getElementById('progressErrors');
if (progressBar) {
progressBar.style.width = percentage + '%';
}
if (progressPercentage) {
progressPercentage.textContent = percentage + '%';
}
if (progressStatus) {
const statusText = status === 'running' ? 'Migrating...' :
status === 'completed' ? 'Migration completed!' :
status === 'failed' ? 'Migration failed!' : 'Starting...';
progressStatus.textContent = statusText;
}
if (currentCustomer) {
currentCustomer.textContent = currentEmail ? `Current: ${currentEmail}` : `Processing ${current} of ${total}`;
}
if (progressAdded) {
progressAdded.textContent = added;
}
if (progressUpdated) {
progressUpdated.textContent = updated;
}
if (progressErrors) {
progressErrors.textContent = errors;
}
}
function deleteM2Customer(customerId, email, firstname, lastname) {

View File

@ -18,13 +18,27 @@
</div>
<div style="margin-top: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
<button id="dryRunCustomerMigrationBtn" class="btn btn-secondary" onclick="startCustomerMigration(true)">
Run Dry Run (Check for Errors)
</button>
<button id="startCustomerMigrationBtn" class="btn btn-primary" onclick="startCustomerMigration(false)">
<button id="startCustomerMigrationBtn" class="btn btn-primary">
Start Customer Migration
</button>
</div>
<!-- Progress Bar -->
<div id="migrationProgressContainer" style="display: none; margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span id="progressStatus" style="font-weight: 600; color: #333;">Starting migration...</span>
<span id="progressPercentage" style="font-weight: 600; color: #1976D2;">0%</span>
</div>
<div style="width: 100%; height: 24px; background-color: #e0e0e0; border-radius: 12px; overflow: hidden;">
<div id="progressBar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #1976D2 0%, #42a5f5 100%); transition: width 0.3s ease; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.85em; font-weight: 600;"></div>
</div>
<div id="progressDetails" style="margin-top: 8px; font-size: 0.9em; color: #666;">
<span id="currentCustomer">-</span> |
Added: <span id="progressAdded">0</span> |
Updated: <span id="progressUpdated">0</span> |
Errors: <span id="progressErrors">0</span>
</div>
</div>
</div>
<!-- Migration Logs Section -->
@ -34,7 +48,7 @@
<div class="log-container" id="customerMigrationLogContainer" style="display: block;">
<div id="customerMigrationLogContent">
<div class="log-entry" style="color: #999; font-style: italic;">
No customer migration logs yet. Click "Run Dry Run" or "Start Customer Migration" to begin.
No customer migration logs yet. Click "Start Customer Migration" to begin.
</div>
</div>
</div>
@ -154,7 +168,8 @@ class="btn btn-danger"
<script>
window.customerRoutes = {
migrateCustomers: '{{ route("customers.migrate-customers") }}',
deleteCustomer: '{{ route("customers.delete-customer", ["customerId" => ":id"]) }}'
deleteCustomer: '{{ route("customers.delete-customer", ["customerId" => ":id"]) }}',
migrationProgress: '{{ route("customers.migration-progress") }}'
};
</script>
@endpush

View File

@ -63,5 +63,6 @@
Route::prefix('customers')->name('customers.')->group(function () {
Route::get('/', [CustomersController::class, 'index'])->name('index');
Route::post('/migrate', [CustomersController::class, 'migrateCustomers'])->name('migrate-customers');
Route::get('/migration-progress', [CustomersController::class, 'getMigrationProgress'])->name('migration-progress');
Route::delete('/{customerId}', [CustomersController::class, 'deleteM2Customer'])->name('delete-customer');
});