option('connection'); $batchSize = max(1, (int) $this->option('batch-size')); $resetProgress = (bool) $this->option('reset-progress'); $dryRun = (bool) $this->option('dry-run'); try { DB::connection($connection)->getPdo(); } catch (\Throwable $e) { $this->error("Cannot connect to Asteria DB (connection='{$connection}'): ".$e->getMessage()); return self::FAILURE; } foreach (['customer_entity', 'eav_attribute'] as $table) { if (! Schema::connection($connection)->hasTable($table)) { $this->error("Asteria table '{$table}' is missing on connection '{$connection}'."); return self::FAILURE; } } if (! Schema::hasColumn('customers', 'migrated_from_asteria_id') || ! Schema::hasColumn('customers', 'legacy_password')) { $this->error('customers.migrated_from_asteria_id / legacy_password are missing. Run php artisan migrate.'); return self::FAILURE; } $groupId = DB::table('customer_groups')->where('code', 'general')->value('id'); if (! $groupId) { $this->error("Bagisto customer group 'general' was not found."); return self::FAILURE; } $channelId = core()->getDefaultChannel()?->id ?? core()->getCurrentChannel()?->id; $reader = new Magento1CustomerReader($connection); $lastId = $resetProgress ? 0 : (int) Cache::get(self::PROGRESS_KEY, 0); if ($resetProgress) { Cache::forget(self::PROGRESS_KEY); } if ($lastId > 0) { $this->line("Resuming from Asteria entity_id > {$lastId} (use --reset-progress to restart)."); } $usedPhones = DB::table('customers') ->whereNotNull('phone') ->where('phone', '!=', '') ->pluck('phone') ->map(fn ($phone) => mb_strtolower((string) $phone)) ->flip() ->all(); $created = 0; $linked = 0; $skipped = 0; $addressesImported = 0; $batchNumber = 0; $this->info($dryRun ? '[DRY RUN] Scanning Magento customers…' : 'Migrating Magento customers…'); do { $customers = $reader->fetchCustomers($lastId, $batchSize); if ($customers->isEmpty()) { break; } $batchNumber++; $lastId = (int) $customers->max('entity_id'); $addresses = $reader->fetchAddresses( $customers->pluck('entity_id')->map(fn ($id) => (int) $id)->all() )->groupBy(fn (array $row) => (int) $row['parent_id']); if ($dryRun) { $created += $customers->count(); $addressesImported += $addresses->flatten(1)->count(); $this->line(sprintf( ' Batch #%d: %d customers, %d addresses (last entity_id=%d) [skipped – dry-run]', $batchNumber, $customers->count(), $addresses->flatten(1)->count(), $lastId )); continue; } $batchCreated = 0; $batchLinked = 0; $batchSkipped = 0; $batchAddresses = 0; DB::transaction(function () use ( $customers, $addresses, $groupId, $channelId, &$usedPhones, &$batchCreated, &$batchLinked, &$batchSkipped, &$batchAddresses ) { foreach ($customers as $row) { $result = $this->migrateCustomer($row, $addresses->get((int) $row['entity_id'], collect()), (int) $groupId, $channelId, $usedPhones); $batchCreated += $result['created']; $batchLinked += $result['linked']; $batchSkipped += $result['skipped']; $batchAddresses += $result['addresses']; } }); Cache::put(self::PROGRESS_KEY, $lastId, now()->addDays(30)); $created += $batchCreated; $linked += $batchLinked; $skipped += $batchSkipped; $addressesImported += $batchAddresses; $this->line(sprintf( ' Batch #%d: created=%d linked=%d skipped=%d addresses=%d (last entity_id=%d)', $batchNumber, $batchCreated, $batchLinked, $batchSkipped, $batchAddresses, $lastId )); Log::info('MigrateAsteriaCustomers: batch '.$batchNumber.', last_id='.$lastId); } while ($customers->count() === $batchSize); $this->newLine(); $this->info("Done. Batches: {$batchNumber}, created: {$created}, linked: {$linked}, skipped: {$skipped}, addresses: {$addressesImported}."); return self::SUCCESS; } /** * @param array $row * @param \Illuminate\Support\Collection> $addressRows * @param array $usedPhones * @return array{created: int, linked: int, skipped: int, addresses: int} */ private function migrateCustomer( array $row, $addressRows, int $groupId, mixed $channelId, array &$usedPhones ): array { $email = strtolower(trim((string) ($row['email'] ?? ''))); $asteriaId = (int) $row['entity_id']; if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { return ['created' => 0, 'linked' => 0, 'skipped' => 1, 'addresses' => 0]; } $existing = Customer::query() ->where(function ($query) use ($asteriaId, $email) { $query->where('migrated_from_asteria_id', $asteriaId) ->orWhereRaw('LOWER(email) = ?', [$email]); }) ->first(); if ($existing) { if (! $existing->migrated_from_asteria_id) { $existing->migrated_from_asteria_id = $asteriaId; $existing->save(); } $imported = $this->importAddresses($existing, $row, $addressRows); return ['created' => 0, 'linked' => 1, 'skipped' => 0, 'addresses' => $imported]; } $phone = $this->uniquePhone((string) ($row['telephone'] ?? ''), $usedPhones); $customer = new Customer; $customer->forceFill([ 'migrated_from_asteria_id' => $asteriaId, 'first_name' => $this->requiredName($row['firstname'] ?? null, $email), 'last_name' => trim((string) ($row['lastname'] ?? '')) ?: '-', 'gender' => $this->mapGender($row['gender'] ?? null), 'date_of_birth' => $this->mapDate($row['dob'] ?? null), 'email' => $email, 'phone' => $phone, 'password' => Hash::make(Str::random(32)), 'legacy_password' => $this->nullableString($row['password_hash'] ?? null), 'api_token' => Str::random(80), 'customer_group_id' => $groupId, 'channel_id' => $channelId, 'subscribed_to_news_letter' => false, 'status' => ((int) ($row['is_active'] ?? 1)) === 1 ? 1 : 0, 'is_verified' => 1, 'is_suspended' => 0, ]); if (! empty($row['created_at'])) { $customer->created_at = $row['created_at']; } $customer->save(); $imported = $this->importAddresses($customer, $row, $addressRows); return ['created' => 1, 'linked' => 0, 'skipped' => 0, 'addresses' => $imported]; } /** * @param array $customerRow * @param \Illuminate\Support\Collection> $addressRows */ private function importAddresses(Customer $customer, array $customerRow, $addressRows): int { if ($addressRows->isEmpty()) { return 0; } $existingIds = $customer->addresses() ->get() ->map(fn (CustomerAddress $address) => $this->asteriaAddressId($address)) ->filter() ->all(); $defaultBilling = (int) ($customerRow['default_billing'] ?? 0); $defaultShipping = (int) ($customerRow['default_shipping'] ?? 0); $imported = 0; foreach ($addressRows as $row) { $asteriaAddressId = (int) $row['entity_id']; if (in_array($asteriaAddressId, $existingIds, true)) { continue; } $address = new CustomerAddress; $address->forceFill([ 'customer_id' => $customer->id, 'address_type' => CustomerAddress::ADDRESS_TYPE, 'first_name' => $this->requiredName($row['firstname'] ?? null, $customer->first_name), 'last_name' => trim((string) ($row['lastname'] ?? '')) ?: $customer->last_name, 'company_name' => $this->nullableString($row['company'] ?? null), 'address' => $this->mapStreet($row['street'] ?? null) ?: '-', 'city' => trim((string) ($row['city'] ?? '')) ?: '-', 'state' => $this->nullableString($row['region'] ?? null), 'country' => $this->nullableString($row['country_id'] ?? null), 'postcode' => $this->nullableString($row['postcode'] ?? null), 'email' => $customer->email, 'phone' => $this->nullableString($row['telephone'] ?? null) ?? $customer->phone, 'default_address' => $defaultBilling > 0 && $asteriaAddressId === $defaultBilling, 'use_for_shipping' => $defaultShipping > 0 && $asteriaAddressId === $defaultShipping, 'additional' => json_encode(['asteria_address_id' => $asteriaAddressId]), ]); $address->save(); $imported++; } return $imported; } /** * @param array $usedPhones */ private function uniquePhone(string $phone, array &$usedPhones): ?string { $phone = trim($phone); if ($phone === '') { return null; } $key = mb_strtolower($phone); if (isset($usedPhones[$key])) { return null; } $usedPhones[$key] = 1; return $phone; } private function mapGender(mixed $value): ?string { return match ((int) $value) { 1 => 'Male', 2 => 'Female', default => null, }; } private function mapDate(mixed $value): ?string { $value = trim((string) $value); if ($value === '' || str_starts_with($value, '0000-00-00')) { return null; } return substr($value, 0, 10); } private function mapStreet(mixed $value): string { $value = trim((string) $value); if ($value === '') { return ''; } $lines = preg_split("/\r\n|\n|\r/", $value) ?: []; return implode(', ', array_filter(array_map('trim', $lines))); } private function requiredName(mixed $value, string $fallback): string { $value = trim((string) $value); if ($value !== '') { return $value; } $local = strstr($fallback, '@', true); return $local !== false && $local !== '' ? $local : 'Customer'; } private function nullableString(mixed $value): ?string { $value = trim((string) $value); return $value === '' ? null : $value; } private function asteriaAddressId(CustomerAddress $address): ?int { $additional = $address->additional; if (is_string($additional) && $additional !== '') { $additional = json_decode($additional, true); } if (! is_array($additional)) { return null; } return isset($additional['asteria_address_id']) ? (int) $additional['asteria_address_id'] : null; } }