Просмотр исходного кода

Use Magento IDs as Bagisto primary keys on empty-catalog Asteria migration.

New customers, products, orders, shipments, and reviews keep migrated_from_asteria_id but insert the same Magento entity_id (review_id for reviews) as id, skip non-simple products, and bump MySQL AUTO_INCREMENT after writes.

Co-authored-by: Cursor <cursoragent@cursor.com>
chengwl 4 дней назад
Родитель
Сommit
92d314285f

+ 15 - 0
app/Console/Commands/MigrateAsteriaCustomers.php

@@ -3,6 +3,7 @@
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
 use App\Services\Asteria\Magento1CustomerReader;
 use App\Services\Asteria\Magento1CustomerReader;
+use App\Services\Asteria\MagentoPrimaryKey;
 use Illuminate\Console\Command;
 use Illuminate\Console\Command;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Cache;
@@ -182,6 +183,10 @@ class MigrateAsteriaCustomers extends Command
         $this->newLine();
         $this->newLine();
         $this->info("Done. Batches: {$batchNumber}, created: {$created}, linked: {$linked}, skipped: {$skipped}, addresses: {$addressesImported}, subscriptions: {$subscriptions}.");
         $this->info("Done. Batches: {$batchNumber}, created: {$created}, linked: {$linked}, skipped: {$skipped}, addresses: {$addressesImported}, subscriptions: {$subscriptions}.");
 
 
+        if (! $dryRun) {
+            MagentoPrimaryKey::bumpAutoIncrement('customers');
+        }
+
         return self::SUCCESS;
         return self::SUCCESS;
     }
     }
 
 
@@ -210,6 +215,7 @@ class MigrateAsteriaCustomers extends Command
             ->all();
             ->all();
 
 
         [$byAsteriaId, $byEmail] = $this->loadExistingCustomers($asteriaIds, $emails);
         [$byAsteriaId, $byEmail] = $this->loadExistingCustomers($asteriaIds, $emails);
+        $occupiedIds = MagentoPrimaryKey::occupiedIds('customers', $asteriaIds);
 
 
         $insertCustomers = [];
         $insertCustomers = [];
         $linkUpdates = [];
         $linkUpdates = [];
@@ -251,11 +257,20 @@ class MigrateAsteriaCustomers extends Command
                 continue;
                 continue;
             }
             }
 
 
+            if (isset($occupiedIds[$asteriaId])) {
+                $skipped++;
+                $this->warn("Customer {$email} skipped: customers.id {$asteriaId} is already occupied.");
+                Log::warning("MigrateAsteriaCustomers: customers.id {$asteriaId} already occupied; skipping Magento customer {$asteriaId} ({$email}).");
+
+                continue;
+            }
+
             $seenEmails[$email] = true;
             $seenEmails[$email] = true;
             $phone = $this->uniquePhone((string) ($row['telephone'] ?? ''), $this->usedPhones);
             $phone = $this->uniquePhone((string) ($row['telephone'] ?? ''), $this->usedPhones);
             $subscriber = $subscribersByCustomerId[$asteriaId] ?? $subscribersByEmail[$email] ?? null;
             $subscriber = $subscribersByCustomerId[$asteriaId] ?? $subscribersByEmail[$email] ?? null;
 
 
             $insertCustomers[$asteriaId] = [
             $insertCustomers[$asteriaId] = [
+                'id'                        => $asteriaId,
                 'migrated_from_asteria_id'  => $asteriaId,
                 'migrated_from_asteria_id'  => $asteriaId,
                 'first_name'                => $this->requiredName($row['firstname'] ?? null, $email),
                 'first_name'                => $this->requiredName($row['firstname'] ?? null, $email),
                 'last_name'                 => trim((string) ($row['lastname'] ?? '')) ?: '-',
                 'last_name'                 => trim((string) ($row['lastname'] ?? '')) ?: '-',

+ 27 - 0
app/Console/Commands/MigrateAsteriaOrders.php

@@ -3,6 +3,7 @@
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
 use App\Services\Asteria\Magento1OrderReader;
 use App\Services\Asteria\Magento1OrderReader;
+use App\Services\Asteria\MagentoPrimaryKey;
 use Illuminate\Console\Command;
 use Illuminate\Console\Command;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Cache;
@@ -239,6 +240,12 @@ class MigrateAsteriaOrders extends Command
             if ($linked > 0) {
             if ($linked > 0) {
                 $this->info("Backfilled product_id on {$linked} existing order items.");
                 $this->info("Backfilled product_id on {$linked} existing order items.");
             }
             }
+
+            MagentoPrimaryKey::bumpAutoIncrement('orders');
+
+            if (Schema::hasColumn('shipments', 'migrated_from_asteria_id')) {
+                MagentoPrimaryKey::bumpAutoIncrement('shipments');
+            }
         }
         }
 
 
         return self::SUCCESS;
         return self::SUCCESS;
@@ -293,6 +300,8 @@ class MigrateAsteriaOrders extends Command
                 ->flip()
                 ->flip()
                 ->all();
                 ->all();
 
 
+        $occupiedIds = MagentoPrimaryKey::occupiedIds('orders', $asteriaIds);
+
         $now = now()->format('Y-m-d H:i:s');
         $now = now()->format('Y-m-d H:i:s');
         $orderInserts = [];
         $orderInserts = [];
         $pending = [];
         $pending = [];
@@ -312,6 +321,14 @@ class MigrateAsteriaOrders extends Command
                 continue;
                 continue;
             }
             }
 
 
+            if (isset($occupiedIds[$asteriaId])) {
+                $skipped++;
+                $this->warn("Order {$incrementId} skipped: orders.id {$asteriaId} is already occupied.");
+                Log::warning("MigrateAsteriaOrders: orders.id {$asteriaId} already occupied; skipping Magento order {$asteriaId}.");
+
+                continue;
+            }
+
             $seenIncrements[$incrementId] = true;
             $seenIncrements[$incrementId] = true;
             $orderInserts[] = $this->buildOrderRow(
             $orderInserts[] = $this->buildOrderRow(
                 $row,
                 $row,
@@ -738,6 +755,8 @@ class MigrateAsteriaOrders extends Command
                 ->flip()
                 ->flip()
                 ->all();
                 ->all();
 
 
+        $occupiedIds = MagentoPrimaryKey::occupiedIds('shipments', $asteriaShipmentIds);
+
         $ordersByAsteriaId = $bagistoOrders->keyBy(fn ($order) => (int) $order->migrated_from_asteria_id);
         $ordersByAsteriaId = $bagistoOrders->keyBy(fn ($order) => (int) $order->migrated_from_asteria_id);
         $shippingAddressIds = DB::table('addresses')
         $shippingAddressIds = DB::table('addresses')
             ->whereIn('order_id', array_values($orderIdMap))
             ->whereIn('order_id', array_values($orderIdMap))
@@ -761,10 +780,17 @@ class MigrateAsteriaOrders extends Command
                 continue;
                 continue;
             }
             }
 
 
+            if (isset($occupiedIds[$asteriaId])) {
+                Log::warning("MigrateAsteriaOrders: shipments.id {$asteriaId} already occupied; skipping Magento shipment {$asteriaId}.");
+
+                continue;
+            }
+
             $tracks = $shipmentTracks->get($asteriaId, collect());
             $tracks = $shipmentTracks->get($asteriaId, collect());
             [$carrierCode, $carrierTitle, $trackNumber] = $this->mapShipmentTracks($tracks);
             [$carrierCode, $carrierTitle, $trackNumber] = $this->mapShipmentTracks($tracks);
 
 
             $inserts[] = [
             $inserts[] = [
+                'id'                       => $asteriaId,
                 'migrated_from_asteria_id' => $asteriaId,
                 'migrated_from_asteria_id' => $asteriaId,
                 'status'                   => null,
                 'status'                   => null,
                 'total_qty'                => $this->qty($row['total_qty'] ?? 0),
                 'total_qty'                => $this->qty($row['total_qty'] ?? 0),
@@ -985,6 +1011,7 @@ class MigrateAsteriaOrders extends Command
             : $baseShippingAmount + $baseShippingTaxAmount;
             : $baseShippingAmount + $baseShippingTaxAmount;
 
 
         $insert = [
         $insert = [
+            'id'                            => (int) $row['entity_id'],
             'migrated_from_asteria_id'      => (int) $row['entity_id'],
             'migrated_from_asteria_id'      => (int) $row['entity_id'],
             'increment_id'                  => trim((string) $row['increment_id']),
             'increment_id'                  => trim((string) $row['increment_id']),
             'status'                        => $this->mapStatus($row),
             'status'                        => $this->mapStatus($row),

+ 38 - 1
app/Console/Commands/MigrateAsteriaProducts.php

@@ -3,6 +3,7 @@
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
 use App\Services\Asteria\Magento1ProductReader;
 use App\Services\Asteria\Magento1ProductReader;
+use App\Services\Asteria\MagentoPrimaryKey;
 use Illuminate\Console\Command;
 use Illuminate\Console\Command;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Cache;
@@ -203,7 +204,9 @@ class MigrateAsteriaProducts extends Command
             $lastId = (int) $products->max('entity_id');
             $lastId = (int) $products->max('entity_id');
 
 
             if ($dryRun) {
             if ($dryRun) {
-                $created += $products->count();
+                $simpleCount = $products->filter(fn (array $row) => $this->isSimpleMagentoProduct($row))->count();
+                $created += $simpleCount;
+                $skipped += $products->count() - $simpleCount;
                 $this->line(sprintf(
                 $this->line(sprintf(
                     '  Batch #%d: %d products (last entity_id=%d) [skipped – dry-run] (%.1fs)',
                     '  Batch #%d: %d products (last entity_id=%d) [skipped – dry-run] (%.1fs)',
                     $batchNumber,
                     $batchNumber,
@@ -251,6 +254,10 @@ class MigrateAsteriaProducts extends Command
         $this->newLine();
         $this->newLine();
         $this->info("Done. Batches: {$batchNumber}, created: {$created}, updated: {$updated}, skipped: {$skipped}.");
         $this->info("Done. Batches: {$batchNumber}, created: {$created}, updated: {$updated}, skipped: {$skipped}.");
 
 
+        if (! $dryRun && ! $this->option('attributes-only')) {
+            MagentoPrimaryKey::bumpAutoIncrement('products');
+        }
+
         return self::SUCCESS;
         return self::SUCCESS;
     }
     }
 
 
@@ -357,6 +364,15 @@ class MigrateAsteriaProducts extends Command
                 continue;
                 continue;
             }
             }
 
 
+            if (! $this->isSimpleMagentoProduct($row)) {
+                $typeId = (string) ($row['type_id'] ?? '');
+                $skipped++;
+                $this->warn("SKU {$sku} skipped: Magento type_id '{$typeId}' is not simple.");
+                Log::warning("MigrateAsteriaProducts: skipping non-simple SKU {$sku} (type_id={$typeId}).");
+
+                continue;
+            }
+
             $variantCount = is_array($row['variants'] ?? null) ? count($row['variants']) : 0;
             $variantCount = is_array($row['variants'] ?? null) ? count($row['variants']) : 0;
 
 
             if ($variantCount > $warnAt) {
             if ($variantCount > $warnAt) {
@@ -396,12 +412,25 @@ class MigrateAsteriaProducts extends Command
 
 
         $wasNew = ! $product->exists;
         $wasNew = ! $product->exists;
 
 
+        if ($wasNew) {
+            if (isset(MagentoPrimaryKey::occupiedIds('products', [$asteriaId])[$asteriaId])) {
+                throw new \RuntimeException("Product id {$asteriaId} is already occupied; refusing to overwrite for SKU {$sku}.");
+            }
+
+            $product->id = $asteriaId;
+            $product->incrementing = false;
+        }
+
         $product->sku = $sku;
         $product->sku = $sku;
         $product->type = 'flexible_variant';
         $product->type = 'flexible_variant';
         $product->attribute_family_id = $this->familyId;
         $product->attribute_family_id = $this->familyId;
         $product->migrated_from_asteria_id = $asteriaId;
         $product->migrated_from_asteria_id = $asteriaId;
         $product->save();
         $product->save();
 
 
+        if ($wasNew) {
+            $product->incrementing = true;
+        }
+
         if ($wasNew && ! empty($row['created_at'])) {
         if ($wasNew && ! empty($row['created_at'])) {
             DB::table('products')->where('id', $product->id)->update([
             DB::table('products')->where('id', $product->id)->update([
                 'created_at' => $row['created_at'],
                 'created_at' => $row['created_at'],
@@ -423,6 +452,14 @@ class MigrateAsteriaProducts extends Command
         return $wasNew;
         return $wasNew;
     }
     }
 
 
+    /**
+     * @param  array<string, mixed>  $row
+     */
+    private function isSimpleMagentoProduct(array $row): bool
+    {
+        return strtolower(trim((string) ($row['type_id'] ?? 'simple'))) === 'simple';
+    }
+
     /**
     /**
      * @param  array<string, mixed>  $row
      * @param  array<string, mixed>  $row
      */
      */

+ 22 - 5
app/Jobs/MigrateReviewJob.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Jobs;
 namespace App\Jobs;
 
 
+use App\Services\Asteria\MagentoPrimaryKey;
 use Illuminate\Bus\Queueable;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -63,8 +64,15 @@ class MigrateReviewJob implements ShouldQueue
             }
             }
 
 
             // Deduplicate: skip if already migrated (keyed on migrated_from_asteria_id).
             // Deduplicate: skip if already migrated (keyed on migrated_from_asteria_id).
+            $reviewId = (int) $row['review_id'];
+
+            if ($reviewId < 1) {
+                $skipped++;
+                continue;
+            }
+
             $exists = DB::table('product_reviews')
             $exists = DB::table('product_reviews')
-                ->where('migrated_from_asteria_id', $row['review_id'])
+                ->where('migrated_from_asteria_id', $reviewId)
                 ->exists();
                 ->exists();
 
 
             if ($exists) {
             if ($exists) {
@@ -72,6 +80,12 @@ class MigrateReviewJob implements ShouldQueue
                 continue;
                 continue;
             }
             }
 
 
+            if (isset(MagentoPrimaryKey::occupiedIds('product_reviews', [$reviewId])[$reviewId])) {
+                Log::warning("MigrateReviewJob: product_reviews.id {$reviewId} already occupied; skipping Magento review {$reviewId}.");
+                $skipped++;
+                continue;
+            }
+
             $rating = $this->normaliseRating($row['avg_rating']);
             $rating = $this->normaliseRating($row['avg_rating']);
             $status = $statusMap[$row['status_id']] ?? 'pending';
             $status = $statusMap[$row['status_id']] ?? 'pending';
             $customerId = $row['customer_email']
             $customerId = $row['customer_email']
@@ -83,6 +97,7 @@ class MigrateReviewJob implements ShouldQueue
             $name    = $this->truncate((string) ($row['name'] ?? ''), 255) ?: 'Guest';
             $name    = $this->truncate((string) ($row['name'] ?? ''), 255) ?: 'Guest';
 
 
             DB::table('product_reviews')->insert([
             DB::table('product_reviews')->insert([
+                'id'                      => $reviewId,
                 'title'                   => $title,
                 'title'                   => $title,
                 'comment'                 => $comment,
                 'comment'                 => $comment,
                 'rating'                  => $rating,
                 'rating'                  => $rating,
@@ -90,21 +105,23 @@ class MigrateReviewJob implements ShouldQueue
                 'product_id'              => $bagistoProductId,
                 'product_id'              => $bagistoProductId,
                 'customer_id'             => $customerId,
                 'customer_id'             => $customerId,
                 'name'                    => $name,
                 'name'                    => $name,
-                'migrated_from_asteria_id' => $row['review_id'],
+                'migrated_from_asteria_id' => $reviewId,
                 'created_at'              => $row['created_at'] ?? $now,
                 'created_at'              => $row['created_at'] ?? $now,
                 'updated_at'              => $now,
                 'updated_at'              => $now,
             ]);
             ]);
 
 
-            $newReviewId = DB::getPdo()->lastInsertId();
-
             // Download and store review images from review_media_image.url
             // Download and store review images from review_media_image.url
             if (! empty($row['images'])) {
             if (! empty($row['images'])) {
-                $this->migrateImages((int) $newReviewId, $row['images']);
+                $this->migrateImages($reviewId, $row['images']);
             }
             }
 
 
             $inserted++;
             $inserted++;
         }
         }
 
 
+        if ($inserted > 0) {
+            MagentoPrimaryKey::bumpAutoIncrement('product_reviews');
+        }
+
         Log::info("MigrateReviewJob: inserted={$inserted}, skipped={$skipped}");
         Log::info("MigrateReviewJob: inserted={$inserted}, skipped={$skipped}");
     }
     }
 
 

+ 55 - 0
app/Services/Asteria/MagentoPrimaryKey.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Services\Asteria;
+
+use Illuminate\Support\Facades\DB;
+
+/**
+ * Empty-catalog Asteria migration writes Magento entity_id / review_id as Bagisto PK.
+ */
+class MagentoPrimaryKey
+{
+    /**
+     * @param  array<int|string>  $ids
+     * @return array<int, int> occupied id => id
+     */
+    public static function occupiedIds(string $table, array $ids): array
+    {
+        $ids = array_values(array_unique(array_filter(
+            array_map('intval', $ids),
+            fn (int $id) => $id > 0
+        )));
+
+        if ($ids === []) {
+            return [];
+        }
+
+        return DB::table($table)
+            ->whereIn('id', $ids)
+            ->pluck('id')
+            ->map(fn ($id) => (int) $id)
+            ->flip()
+            ->all();
+    }
+
+    public static function bumpAutoIncrement(string $table): void
+    {
+        $driver = DB::connection()->getDriverName();
+
+        if (! in_array($driver, ['mysql', 'mariadb'], true)) {
+            return;
+        }
+
+        // DDL implicit-commits MySQL, which would leak rows from PHPUnit DatabaseTransactions.
+        if (DB::connection()->transactionLevel() > 0) {
+            return;
+        }
+
+        $max = (int) DB::table($table)->max('id');
+        $next = max(1, $max + 1);
+        $prefix = DB::connection()->getTablePrefix();
+        $safe = str_replace(['`', '.', ' '], '', $table);
+
+        DB::statement('ALTER TABLE `'.$prefix.$safe.'` AUTO_INCREMENT = '.$next);
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 31 - 14
docs/asteria-migration.md


+ 60 - 32
packages/Webkul/BagistoApi/tests/Unit/Migration/MigrateAsteriaCustomersCommandTest.php

@@ -18,6 +18,9 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
 
 
     private string $connection = 'asteria_test';
     private string $connection = 'asteria_test';
 
 
+    /** Magento entity_ids in a range unlikely to collide with a populated Bagisto catalog. */
+    private const MID = 881900000;
+
     public function setUp(): void
     public function setUp(): void
     {
     {
         parent::setUp();
         parent::setUp();
@@ -29,6 +32,7 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
         }
         }
 
 
         $this->seedRequiredData();
         $this->seedRequiredData();
+        $this->forgetLeakedAsteriaCustomers();
         Cache::forget('migrate_asteria_customers_last_id');
         Cache::forget('migrate_asteria_customers_last_id');
 
 
         $this->sqlitePath = sys_get_temp_dir().'/asteria_cmd_'.uniqid('', true).'.sqlite';
         $this->sqlitePath = sys_get_temp_dir().'/asteria_cmd_'.uniqid('', true).'.sqlite';
@@ -59,22 +63,22 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
     public function test_it_migrates_a_customer_and_address_into_the_general_group(): void
     public function test_it_migrates_a_customer_and_address_into_the_general_group(): void
     {
     {
         MagentoSchema::seedCustomer($this->connection, [
         MagentoSchema::seedCustomer($this->connection, [
-            'entity_id'        => 10,
-            'email'            => 'jane@example.com',
+            'entity_id'        => self::MID + 10,
+            'email'            => 'mig-pk-jane@example.test',
             'firstname'        => 'Jane',
             'firstname'        => 'Jane',
             'lastname'         => 'Doe',
             'lastname'         => 'Doe',
-            'telephone'        => '5551112222',
+            'telephone'        => '5558819010',
             'gender'           => 1,
             'gender'           => 1,
             'dob'              => '1990-05-01 00:00:00',
             'dob'              => '1990-05-01 00:00:00',
             'password_hash'    => md5('abcsecret12').':abc',
             'password_hash'    => md5('abcsecret12').':abc',
             'group_id'         => 99,
             'group_id'         => 99,
-            'default_billing'  => 21,
-            'default_shipping' => 21,
+            'default_billing'  => self::MID + 21,
+            'default_shipping' => self::MID + 21,
             'source'           => 20,
             'source'           => 20,
         ]);
         ]);
         MagentoSchema::seedAddress($this->connection, [
         MagentoSchema::seedAddress($this->connection, [
-            'entity_id'  => 21,
-            'parent_id'  => 10,
+            'entity_id'  => self::MID + 21,
+            'parent_id'  => self::MID + 10,
             'firstname'  => 'Jane',
             'firstname'  => 'Jane',
             'lastname'   => 'Doe',
             'lastname'   => 'Doe',
             'street'     => "123 Main St\nApt 4",
             'street'     => "123 Main St\nApt 4",
@@ -82,7 +86,7 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
             'region'     => 'TX',
             'region'     => 'TX',
             'postcode'   => '78701',
             'postcode'   => '78701',
             'country_id' => 'US',
             'country_id' => 'US',
-            'telephone'  => '5551112222',
+            'telephone'  => '5558819010',
             'company'    => 'Acme',
             'company'    => 'Acme',
         ]);
         ]);
 
 
@@ -91,14 +95,16 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $customer = Customer::query()->where('email', 'jane@example.com')->first();
+        $customer = Customer::query()->where('email', 'mig-pk-jane@example.test')->first();
         $this->assertNotNull($customer);
         $this->assertNotNull($customer);
-        $this->assertSame(10, (int) $customer->migrated_from_asteria_id);
+        $this->assertSame(self::MID + 10, (int) $customer->id);
+        $this->assertSame(self::MID + 10, (int) $customer->migrated_from_asteria_id);
+        $this->assertSame((int) $customer->id, (int) $customer->migrated_from_asteria_id);
         $this->assertSame('Jane', $customer->first_name);
         $this->assertSame('Jane', $customer->first_name);
         $this->assertSame('Doe', $customer->last_name);
         $this->assertSame('Doe', $customer->last_name);
         $this->assertSame('Male', $customer->gender);
         $this->assertSame('Male', $customer->gender);
         $this->assertSame('1990-05-01', $customer->date_of_birth);
         $this->assertSame('1990-05-01', $customer->date_of_birth);
-        $this->assertSame('5551112222', $customer->phone);
+        $this->assertSame('5558819010', $customer->phone);
         $this->assertSame(md5('abcsecret12').':abc', $customer->legacy_password);
         $this->assertSame(md5('abcsecret12').':abc', $customer->legacy_password);
         $this->assertSame(1, (int) $customer->is_verified);
         $this->assertSame(1, (int) $customer->is_verified);
         $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
         $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
@@ -114,13 +120,13 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
         $this->assertSame('US', $address->country);
         $this->assertSame('US', $address->country);
         $this->assertTrue((bool) $address->default_address);
         $this->assertTrue((bool) $address->default_address);
         $this->assertTrue((bool) $address->use_for_shipping);
         $this->assertTrue((bool) $address->use_for_shipping);
-        $this->assertSame(21, $this->asteriaAddressId($address));
+        $this->assertSame(self::MID + 21, $this->asteriaAddressId($address));
     }
     }
 
 
     public function test_dry_run_does_not_write_customers(): void
     public function test_dry_run_does_not_write_customers(): void
     {
     {
         MagentoSchema::seedCustomer($this->connection, [
         MagentoSchema::seedCustomer($this->connection, [
-            'entity_id' => 10,
+            'entity_id' => self::MID + 10,
             'email'     => 'dry-run@example.com',
             'email'     => 'dry-run@example.com',
             'firstname' => 'Dry',
             'firstname' => 'Dry',
             'lastname'  => 'Run',
             'lastname'  => 'Run',
@@ -141,21 +147,21 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
     public function test_it_links_an_existing_email_without_overwriting_the_password(): void
     public function test_it_links_an_existing_email_without_overwriting_the_password(): void
     {
     {
         $existing = $this->createCustomer([
         $existing = $this->createCustomer([
-            'email'     => 'existing@example.com',
+            'email'     => 'mig-pk-existing@example.test',
             'password'  => Hash::make('bagisto-secret'),
             'password'  => Hash::make('bagisto-secret'),
             'first_name'=> 'Keep',
             'first_name'=> 'Keep',
         ]);
         ]);
 
 
         MagentoSchema::seedCustomer($this->connection, [
         MagentoSchema::seedCustomer($this->connection, [
-            'entity_id'     => 44,
-            'email'         => 'existing@example.com',
+            'entity_id'     => self::MID + 44,
+            'email'         => 'mig-pk-existing@example.test',
             'firstname'     => 'Magento',
             'firstname'     => 'Magento',
             'lastname'      => 'Name',
             'lastname'      => 'Name',
             'password_hash' => md5('abcsecret12').':abc',
             'password_hash' => md5('abcsecret12').':abc',
         ]);
         ]);
         MagentoSchema::seedAddress($this->connection, [
         MagentoSchema::seedAddress($this->connection, [
-            'entity_id'  => 45,
-            'parent_id'  => 44,
+            'entity_id'  => self::MID + 45,
+            'parent_id'  => self::MID + 44,
             'firstname'  => 'Magento',
             'firstname'  => 'Magento',
             'lastname'   => 'Name',
             'lastname'   => 'Name',
             'street'     => '9 Oak Rd',
             'street'     => '9 Oak Rd',
@@ -163,6 +169,8 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
             'country_id' => 'US',
             'country_id' => 'US',
         ]);
         ]);
 
 
+        $bagistoId = (int) $existing->id;
+
         $this->artisan('customers:migrate-asteria', [
         $this->artisan('customers:migrate-asteria', [
             '--connection'     => $this->connection,
             '--connection'     => $this->connection,
             '--reset-progress' => true,
             '--reset-progress' => true,
@@ -170,27 +178,28 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
 
 
         $existing->refresh();
         $existing->refresh();
 
 
-        $this->assertSame(44, (int) $existing->migrated_from_asteria_id);
+        $this->assertSame($bagistoId, (int) $existing->id);
+        $this->assertSame(self::MID + 44, (int) $existing->migrated_from_asteria_id);
         $this->assertSame('Keep', $existing->first_name);
         $this->assertSame('Keep', $existing->first_name);
         $this->assertTrue(Hash::check('bagisto-secret', $existing->password));
         $this->assertTrue(Hash::check('bagisto-secret', $existing->password));
         $this->assertNull($existing->legacy_password);
         $this->assertNull($existing->legacy_password);
-        $this->assertSame(1, Customer::query()->where('email', 'existing@example.com')->count());
+        $this->assertSame(1, Customer::query()->where('email', 'mig-pk-existing@example.test')->count());
         $this->assertSame(1, CustomerAddress::query()->where('customer_id', $existing->id)->count());
         $this->assertSame(1, CustomerAddress::query()->where('customer_id', $existing->id)->count());
     }
     }
 
 
     public function test_it_nulls_a_conflicting_phone_number(): void
     public function test_it_nulls_a_conflicting_phone_number(): void
     {
     {
         $this->createCustomer([
         $this->createCustomer([
-            'email' => 'owner@example.com',
-            'phone' => '5550001111',
+            'email' => 'mig-pk-owner@example.test',
+            'phone' => '5558819070',
         ]);
         ]);
 
 
         MagentoSchema::seedCustomer($this->connection, [
         MagentoSchema::seedCustomer($this->connection, [
-            'entity_id' => 70,
-            'email'     => 'other@example.com',
+            'entity_id' => self::MID + 70,
+            'email'     => 'mig-pk-other@example.test',
             'firstname' => 'Other',
             'firstname' => 'Other',
             'lastname'  => 'Person',
             'lastname'  => 'Person',
-            'telephone' => '5550001111',
+            'telephone' => '5558819070',
         ]);
         ]);
 
 
         $this->artisan('customers:migrate-asteria', [
         $this->artisan('customers:migrate-asteria', [
@@ -198,7 +207,7 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $migrated = Customer::query()->where('email', 'other@example.com')->first();
+        $migrated = Customer::query()->where('email', 'mig-pk-other@example.test')->first();
         $this->assertNotNull($migrated);
         $this->assertNotNull($migrated);
         $this->assertNull($migrated->phone);
         $this->assertNull($migrated->phone);
     }
     }
@@ -206,14 +215,14 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
     public function test_it_is_idempotent_on_a_second_run(): void
     public function test_it_is_idempotent_on_a_second_run(): void
     {
     {
         MagentoSchema::seedCustomer($this->connection, [
         MagentoSchema::seedCustomer($this->connection, [
-            'entity_id' => 80,
-            'email'     => 'once@example.com',
+            'entity_id' => self::MID + 80,
+            'email'     => 'mig-pk-once@example.test',
             'firstname' => 'Once',
             'firstname' => 'Once',
             'lastname'  => 'Only',
             'lastname'  => 'Only',
         ]);
         ]);
         MagentoSchema::seedAddress($this->connection, [
         MagentoSchema::seedAddress($this->connection, [
-            'entity_id'  => 81,
-            'parent_id'  => 80,
+            'entity_id'  => self::MID + 81,
+            'parent_id'  => self::MID + 80,
             'firstname'  => 'Once',
             'firstname'  => 'Once',
             'lastname'   => 'Only',
             'lastname'   => 'Only',
             'street'     => '1 Repeat Ln',
             'street'     => '1 Repeat Ln',
@@ -229,8 +238,10 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
         $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
 
 
-        $this->assertSame(1, Customer::query()->where('email', 'once@example.com')->count());
-        $customer = Customer::query()->where('email', 'once@example.com')->first();
+        $this->assertSame(1, Customer::query()->where('email', 'mig-pk-once@example.test')->count());
+        $customer = Customer::query()->where('email', 'mig-pk-once@example.test')->first();
+        $this->assertSame(self::MID + 80, (int) $customer->id);
+        $this->assertSame(self::MID + 80, (int) $customer->migrated_from_asteria_id);
         $this->assertSame(1, CustomerAddress::query()->where('customer_id', $customer->id)->count());
         $this->assertSame(1, CustomerAddress::query()->where('customer_id', $customer->id)->count());
     }
     }
 
 
@@ -501,4 +512,21 @@ class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
 
 
         return is_array($additional) ? (int) ($additional['asteria_address_id'] ?? 0) : null;
         return is_array($additional) ? (int) ($additional['asteria_address_id'] ?? 0) : null;
     }
     }
+
+    private function forgetLeakedAsteriaCustomers(): void
+    {
+        $ids = Customer::query()
+            ->where('email', 'like', 'mig-pk-%')
+            ->orWhere('email', 'like', 'asteria-%')
+            ->pluck('id');
+
+        if ($ids->isEmpty()) {
+            return;
+        }
+
+        $emails = Customer::query()->whereIn('id', $ids)->pluck('email');
+        CustomerAddress::query()->whereIn('customer_id', $ids)->delete();
+        SubscribersList::query()->whereIn('email', $emails)->delete();
+        Customer::query()->whereIn('id', $ids)->delete();
+    }
 }
 }

+ 75 - 42
packages/Webkul/BagistoApi/tests/Unit/Migration/MigrateAsteriaOrdersCommandTest.php

@@ -20,6 +20,8 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
 
 
     private string $connection = 'asteria_test';
     private string $connection = 'asteria_test';
 
 
+    private const MID = 881900000;
+
     public function setUp(): void
     public function setUp(): void
     {
     {
         parent::setUp();
         parent::setUp();
@@ -29,6 +31,7 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
         }
         }
 
 
         $this->seedRequiredData();
         $this->seedRequiredData();
+        $this->forgetLeakedAsteriaOrders();
         Cache::forget('migrate_asteria_orders_last_id');
         Cache::forget('migrate_asteria_orders_last_id');
 
 
         $this->sqlitePath = sys_get_temp_dir().'/asteria_order_cmd_'.uniqid('', true).'.sqlite';
         $this->sqlitePath = sys_get_temp_dir().'/asteria_order_cmd_'.uniqid('', true).'.sqlite';
@@ -59,12 +62,12 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_it_migrates_an_order_with_items_addresses_and_payment(): void
     public function test_it_migrates_an_order_with_items_addresses_and_payment(): void
     {
     {
         $customer = $this->createCustomer([
         $customer = $this->createCustomer([
-            'email'                    => 'jane@example.com',
+            'email'                    => 'mig-pk-jane@example.test',
             'first_name'               => 'Jane',
             'first_name'               => 'Jane',
             'last_name'                => 'Doe',
             'last_name'                => 'Doe',
-            'migrated_from_asteria_id' => 10,
+            'migrated_from_asteria_id' => self::MID + 10,
         ]);
         ]);
-        $product = $this->createSimpleProduct('WIG-001');
+        $product = $this->createSimpleProduct('AST-TEST-WIG-001');
 
 
         $this->seedCompleteMagentoOrder();
         $this->seedCompleteMagentoOrder();
 
 
@@ -73,13 +76,15 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $order = Order::query()->where('increment_id', '100000010')->first();
+        $order = Order::query()->where('increment_id', 'T881900010')->first();
         $this->assertNotNull($order);
         $this->assertNotNull($order);
-        $this->assertSame(10, (int) $order->migrated_from_asteria_id);
+        $this->assertSame(self::MID + 10, (int) $order->id);
+        $this->assertSame(self::MID + 10, (int) $order->migrated_from_asteria_id);
+        $this->assertSame((int) $order->id, (int) $order->migrated_from_asteria_id);
         $this->assertSame(Order::STATUS_COMPLETED, $order->status);
         $this->assertSame(Order::STATUS_COMPLETED, $order->status);
         $this->assertSame((int) $customer->id, (int) $order->customer_id);
         $this->assertSame((int) $customer->id, (int) $order->customer_id);
         $this->assertSame(0, (int) $order->is_guest);
         $this->assertSame(0, (int) $order->is_guest);
-        $this->assertSame('jane@example.com', $order->customer_email);
+        $this->assertSame('mig-pk-jane@example.test', $order->customer_email);
         $this->assertEquals(110, (float) $order->grand_total);
         $this->assertEquals(110, (float) $order->grand_total);
         $this->assertEquals(100, (float) $order->sub_total);
         $this->assertEquals(100, (float) $order->sub_total);
         $this->assertEquals(10, (float) $order->shipping_amount);
         $this->assertEquals(10, (float) $order->shipping_amount);
@@ -87,7 +92,7 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
 
 
         $item = OrderItem::query()->where('order_id', $order->id)->first();
         $item = OrderItem::query()->where('order_id', $order->id)->first();
         $this->assertNotNull($item);
         $this->assertNotNull($item);
-        $this->assertSame('WIG-001', $item->sku);
+        $this->assertSame('AST-TEST-WIG-001', $item->sku);
         $this->assertSame('Lace Wig', $item->name);
         $this->assertSame('Lace Wig', $item->name);
         $this->assertSame((int) $product->id, (int) $item->product_id);
         $this->assertSame((int) $product->id, (int) $item->product_id);
         $this->assertSame('simple', $item->type);
         $this->assertSame('simple', $item->type);
@@ -120,8 +125,8 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_dry_run_does_not_write_orders(): void
     public function test_dry_run_does_not_write_orders(): void
     {
     {
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'      => 10,
-            'increment_id'   => '100000010',
+            'entity_id'      => self::MID + 10,
+            'increment_id'   => 'T881900010',
             'customer_email' => 'dry-run@example.com',
             'customer_email' => 'dry-run@example.com',
         ]);
         ]);
 
 
@@ -134,15 +139,15 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
         $this->assertSame($before, Order::query()->count());
         $this->assertSame($before, Order::query()->count());
-        $this->assertNull(Order::query()->where('increment_id', '100000010')->first());
+        $this->assertNull(Order::query()->where('increment_id', 'T881900010')->first());
     }
     }
 
 
     public function test_it_is_idempotent_on_a_second_run(): void
     public function test_it_is_idempotent_on_a_second_run(): void
     {
     {
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'      => 80,
-            'increment_id'   => '100000080',
-            'customer_email' => 'once@example.com',
+            'entity_id'      => self::MID + 80,
+            'increment_id'   => 'T881900080',
+            'customer_email' => 'mig-pk-once@example.test',
             'items'          => [
             'items'          => [
                 ['item_id' => 801, 'sku' => 'ONCE-1', 'name' => 'Once'],
                 ['item_id' => 801, 'sku' => 'ONCE-1', 'name' => 'Once'],
             ],
             ],
@@ -163,8 +168,10 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
         $this->artisan('orders:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('orders:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('orders:migrate-asteria', $options)->assertSuccessful();
         $this->artisan('orders:migrate-asteria', $options)->assertSuccessful();
 
 
-        $this->assertSame(1, Order::query()->where('increment_id', '100000080')->count());
-        $order = Order::query()->where('increment_id', '100000080')->first();
+        $this->assertSame(1, Order::query()->where('increment_id', 'T881900080')->count());
+        $order = Order::query()->where('increment_id', 'T881900080')->first();
+        $this->assertSame(self::MID + 80, (int) $order->id);
+        $this->assertSame(self::MID + 80, (int) $order->migrated_from_asteria_id);
         $this->assertSame(1, OrderItem::query()->where('order_id', $order->id)->count());
         $this->assertSame(1, OrderItem::query()->where('order_id', $order->id)->count());
         $this->assertSame(1, OrderAddress::query()->where('order_id', $order->id)->count());
         $this->assertSame(1, OrderAddress::query()->where('order_id', $order->id)->count());
         $this->assertSame(1, OrderPayment::query()->where('order_id', $order->id)->count());
         $this->assertSame(1, OrderPayment::query()->where('order_id', $order->id)->count());
@@ -174,15 +181,15 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_it_links_a_customer_by_asteria_id(): void
     public function test_it_links_a_customer_by_asteria_id(): void
     {
     {
         $customer = $this->createCustomer([
         $customer = $this->createCustomer([
-            'email'                    => 'linked@example.com',
-            'migrated_from_asteria_id' => 44,
+            'email'                    => 'mig-pk-linked@example.test',
+            'migrated_from_asteria_id' => self::MID + 44,
         ]);
         ]);
 
 
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'      => 90,
-            'increment_id'   => '100000090',
-            'customer_id'    => 44,
-            'customer_email' => 'other-address@example.com',
+            'entity_id'      => self::MID + 90,
+            'increment_id'   => 'T881900090',
+            'customer_id'    => self::MID + 44,
+            'customer_email' => 'mig-pk-other-address@example.test',
             'items'          => [
             'items'          => [
                 ['item_id' => 901, 'sku' => 'LINK-1'],
                 ['item_id' => 901, 'sku' => 'LINK-1'],
             ],
             ],
@@ -194,7 +201,7 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $order = Order::query()->where('increment_id', '100000090')->first();
+        $order = Order::query()->where('increment_id', 'T881900090')->first();
         $this->assertNotNull($order);
         $this->assertNotNull($order);
         $this->assertSame((int) $customer->id, (int) $order->customer_id);
         $this->assertSame((int) $customer->id, (int) $order->customer_id);
         $this->assertSame(0, (int) $order->is_guest);
         $this->assertSame(0, (int) $order->is_guest);
@@ -203,10 +210,10 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_it_creates_a_guest_order_when_the_customer_is_missing(): void
     public function test_it_creates_a_guest_order_when_the_customer_is_missing(): void
     {
     {
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'          => 91,
-            'increment_id'       => '100000091',
-            'customer_id'        => 9999,
-            'customer_email'     => 'nobody@example.com',
+            'entity_id'          => self::MID + 91,
+            'increment_id'       => 'T881900091',
+            'customer_id'        => self::MID + 9999,
+            'customer_email'     => 'mig-pk-nobody@example.test',
             'customer_firstname' => 'Guest',
             'customer_firstname' => 'Guest',
             'customer_lastname'  => 'Buyer',
             'customer_lastname'  => 'Buyer',
             'customer_is_guest'  => 1,
             'customer_is_guest'  => 1,
@@ -221,11 +228,11 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $order = Order::query()->where('increment_id', '100000091')->first();
+        $order = Order::query()->where('increment_id', 'T881900091')->first();
         $this->assertNotNull($order);
         $this->assertNotNull($order);
         $this->assertNull($order->customer_id);
         $this->assertNull($order->customer_id);
         $this->assertSame(1, (int) $order->is_guest);
         $this->assertSame(1, (int) $order->is_guest);
-        $this->assertSame('nobody@example.com', $order->customer_email);
+        $this->assertSame('mig-pk-nobody@example.test', $order->customer_email);
         $this->assertSame('Guest', $order->customer_first_name);
         $this->assertSame('Guest', $order->customer_first_name);
         $this->assertSame('cashondelivery', $order->payment->method);
         $this->assertSame('cashondelivery', $order->payment->method);
     }
     }
@@ -233,8 +240,8 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_it_imports_unmatched_sku_items_without_a_product_id(): void
     public function test_it_imports_unmatched_sku_items_without_a_product_id(): void
     {
     {
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'    => 92,
-            'increment_id' => '100000092',
+            'entity_id'    => self::MID + 92,
+            'increment_id' => 'T881900092',
             'items'        => [
             'items'        => [
                 [
                 [
                     'item_id'      => 921,
                     'item_id'      => 921,
@@ -251,7 +258,7 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $order = Order::query()->where('increment_id', '100000092')->first();
+        $order = Order::query()->where('increment_id', 'T881900092')->first();
         $this->assertNotNull($order);
         $this->assertNotNull($order);
         $item = OrderItem::query()->where('order_id', $order->id)->first();
         $item = OrderItem::query()->where('order_id', $order->id)->first();
         $this->assertSame('MISSING-SKU', $item->sku);
         $this->assertSame('MISSING-SKU', $item->sku);
@@ -264,18 +271,19 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
     public function test_it_skips_when_increment_id_already_exists(): void
     public function test_it_skips_when_increment_id_already_exists(): void
     {
     {
         $channel = Channel::query()->first();
         $channel = Channel::query()->first();
-        Order::factory()->create([
-            'increment_id'  => '100000099',
+        $native = Order::factory()->create([
+            'increment_id'  => 'T881900099',
             'channel_id'    => $channel?->id,
             'channel_id'    => $channel?->id,
             'channel_type'  => Channel::class,
             'channel_type'  => Channel::class,
             'customer_id'   => null,
             'customer_id'   => null,
             'is_guest'      => 1,
             'is_guest'      => 1,
             'customer_email'=> 'native@example.com',
             'customer_email'=> 'native@example.com',
         ]);
         ]);
+        $bagistoId = (int) $native->id;
 
 
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'      => 99,
-            'increment_id'   => '100000099',
+            'entity_id'      => self::MID + 99,
+            'increment_id'   => 'T881900099',
             'customer_email' => 'asteria@example.com',
             'customer_email' => 'asteria@example.com',
             'items'          => [
             'items'          => [
                 ['item_id' => 991, 'sku' => 'SKIP-1'],
                 ['item_id' => 991, 'sku' => 'SKIP-1'],
@@ -288,19 +296,20 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             '--reset-progress' => true,
             '--reset-progress' => true,
         ])->assertSuccessful();
         ])->assertSuccessful();
 
 
-        $this->assertSame(1, Order::query()->where('increment_id', '100000099')->count());
-        $existing = Order::query()->where('increment_id', '100000099')->first();
+        $this->assertSame(1, Order::query()->where('increment_id', 'T881900099')->count());
+        $existing = Order::query()->where('increment_id', 'T881900099')->first();
         $this->assertNull($existing->migrated_from_asteria_id);
         $this->assertNull($existing->migrated_from_asteria_id);
         $this->assertSame('native@example.com', $existing->customer_email);
         $this->assertSame('native@example.com', $existing->customer_email);
+        $this->assertSame($bagistoId, (int) $existing->id);
     }
     }
 
 
     private function seedCompleteMagentoOrder(): void
     private function seedCompleteMagentoOrder(): void
     {
     {
         MagentoSchema::seedOrder($this->connection, [
         MagentoSchema::seedOrder($this->connection, [
-            'entity_id'          => 10,
-            'increment_id'       => '100000010',
-            'customer_id'        => 10,
-            'customer_email'     => 'jane@example.com',
+            'entity_id'          => self::MID + 10,
+            'increment_id'       => 'T881900010',
+            'customer_id'        => self::MID + 10,
+            'customer_email'     => 'mig-pk-jane@example.test',
             'customer_firstname' => 'Jane',
             'customer_firstname' => 'Jane',
             'customer_lastname'  => 'Doe',
             'customer_lastname'  => 'Doe',
             'status'             => 'complete',
             'status'             => 'complete',
@@ -312,7 +321,7 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             'items'              => [
             'items'              => [
                 [
                 [
                     'item_id'    => 101,
                     'item_id'    => 101,
-                    'sku'        => 'WIG-001',
+                    'sku'        => 'AST-TEST-WIG-001',
                     'name'       => 'Lace Wig',
                     'name'       => 'Lace Wig',
                     'qty_ordered'=> 1,
                     'qty_ordered'=> 1,
                     'price'      => 100,
                     'price'      => 100,
@@ -357,4 +366,28 @@ class MigrateAsteriaOrdersCommandTest extends BagistoApiTestCase
             'attribute_family_id' => $attributeFamilyId,
             'attribute_family_id' => $attributeFamilyId,
         ]);
         ]);
     }
     }
+
+    private function forgetLeakedAsteriaOrders(): void
+    {
+        $orderIds = Order::query()
+            ->where('increment_id', 'like', 'T8819%')
+            ->pluck('id');
+
+        if ($orderIds->isNotEmpty()) {
+            OrderItem::query()->whereIn('order_id', $orderIds)->delete();
+            OrderAddress::query()->whereIn('order_id', $orderIds)->delete();
+            OrderPayment::query()->whereIn('order_id', $orderIds)->delete();
+            Order::query()->whereIn('id', $orderIds)->delete();
+        }
+
+        $customerIds = Customer::query()
+            ->where('email', 'like', 'mig-pk-%')
+            ->pluck('id');
+
+        if ($customerIds->isNotEmpty()) {
+            Customer::query()->whereIn('id', $customerIds)->delete();
+        }
+
+        Product::query()->where('sku', 'like', 'AST-TEST-%')->delete();
+    }
 }
 }

+ 298 - 0
packages/Webkul/BagistoApi/tests/Unit/Migration/MigrateAsteriaProductsCommandTest.php

@@ -0,0 +1,298 @@
+<?php
+
+namespace Webkul\BagistoApi\Tests\Unit\Migration;
+
+use App\Services\Asteria\Magento1ProductReader;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+use Longyi\Core\Models\ProductVariant;
+use Webkul\Attribute\Models\Attribute;
+use Webkul\BagistoApi\Tests\BagistoApiTestCase;
+use Webkul\Core\Models\Channel;
+use Webkul\Product\Models\Product;
+
+class MigrateAsteriaProductsCommandTest extends BagistoApiTestCase
+{
+    private string $sqlitePath;
+
+    private string $connection = 'asteria_test';
+
+    private const MID = 881900000;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        if (! Schema::hasColumn('products', 'migrated_from_asteria_id')) {
+            $this->markTestSkipped('Run php artisan migrate to add Asteria product columns.');
+        }
+
+        if (! Schema::hasTable('product_options') || ! Schema::hasTable('product_variants')) {
+            $this->markTestSkipped('Flexible variant tables are missing.');
+        }
+
+        if (! DB::table('attribute_families')->exists() || ! DB::table('inventory_sources')->exists()) {
+            $this->markTestSkipped('Run Bagisto seeders for attribute families and inventory sources.');
+        }
+
+        $this->seedRequiredData();
+        $this->forgetLeakedAsteriaProducts();
+        Cache::forget('migrate_asteria_products_last_id');
+
+        $this->sqlitePath = sys_get_temp_dir().'/asteria_product_cmd_'.uniqid('', true).'.sqlite';
+        touch($this->sqlitePath);
+
+        config()->set('database.connections.'.$this->connection, [
+            'driver'                  => 'sqlite',
+            'database'                => $this->sqlitePath,
+            'prefix'                  => '',
+            'foreign_key_constraints' => false,
+        ]);
+        config()->set('asteria.products', array_merge(Magento1ProductReader::defaultConfig(), [
+            'include_eav_attributes' => ['hair_colorr'],
+            'skip_option_titles'     => ['Free Gift'],
+            'variant_option_titles'  => [],
+            'attribute_family'       => 'default',
+            'media_base_url'         => 'https://img.example.com/media/catalog/product',
+            'store_media_url'        => 'https://img.example.com/media/',
+        ]));
+
+        DB::purge($this->connection);
+        MagentoSchema::create($this->connection);
+    }
+
+    public function tearDown(): void
+    {
+        DB::purge($this->connection);
+
+        if (isset($this->sqlitePath) && is_file($this->sqlitePath)) {
+            @unlink($this->sqlitePath);
+        }
+
+        parent::tearDown();
+    }
+
+    public function test_it_migrates_a_product_with_attributes_images_and_variants(): void
+    {
+        $this->seedMagentoProduct();
+
+        $this->artisan('products:migrate-asteria', [
+            '--connection'     => $this->connection,
+            '--reset-progress' => true,
+            '--no-index'       => true,
+        ])->assertSuccessful();
+
+        $product = Product::query()->where('sku', 'AST-TEST-WIG-001')->first();
+        $this->assertNotNull($product);
+        $this->assertSame(self::MID + 10, (int) $product->id);
+        $this->assertSame(self::MID + 10, (int) $product->migrated_from_asteria_id);
+        $this->assertSame((int) $product->id, (int) $product->migrated_from_asteria_id);
+        $this->assertSame('flexible_variant', $product->type);
+
+        $this->assertSame('Lace Front Wig', $this->attributeText($product->id, 'name'));
+        $this->assertEquals(99.0, (float) $this->attributeFloat($product->id, 'price'));
+
+        $this->assertDatabaseHas('product_inventories', [
+            'product_id' => $product->id,
+            'qty'        => 12,
+        ]);
+
+        $this->assertDatabaseHas('product_images', [
+            'product_id' => $product->id,
+            'path'       => 'https://img.example.com/media/catalog/product/w/i/wig.jpg',
+        ]);
+
+        $this->assertSame(4, ProductVariant::query()->where('product_id', $product->id)->count());
+        $this->assertTrue(
+            ProductVariant::query()->where('product_id', $product->id)->where('sku', 'AST-TEST-WIG-001-1-1')->exists()
+        );
+
+        $channelIds = Channel::query()->pluck('id')->map(fn ($id) => (int) $id)->all();
+        $this->assertNotEmpty($channelIds);
+        $this->assertSame(
+            count($channelIds),
+            DB::table('product_channels')->where('product_id', $product->id)->count()
+        );
+        foreach ($channelIds as $channelId) {
+            $this->assertDatabaseHas('product_channels', [
+                'product_id' => $product->id,
+                'channel_id' => $channelId,
+            ]);
+        }
+
+        $hairColor = Attribute::query()->where('code', 'hair_colorr')->first();
+        $this->assertNotNull($hairColor);
+        $this->assertTrue($hairColor->options()->where('admin_name', 'Natural Black')->exists());
+    }
+
+    public function test_dry_run_does_not_write_products(): void
+    {
+        $this->seedMagentoProduct();
+        $before = Product::query()->count();
+
+        $this->artisan('products:migrate-asteria', [
+            '--connection'     => $this->connection,
+            '--dry-run'        => true,
+            '--reset-progress' => true,
+            '--no-index'       => true,
+        ])->assertSuccessful();
+
+        $this->assertSame($before, Product::query()->count());
+        $this->assertNull(Product::query()->where('sku', 'AST-TEST-WIG-001')->first());
+    }
+
+    public function test_re_run_updates_the_same_product_instead_of_duplicating(): void
+    {
+        $this->seedMagentoProduct();
+
+        $options = [
+            '--connection'     => $this->connection,
+            '--reset-progress' => true,
+            '--no-index'       => true,
+        ];
+
+        $this->artisan('products:migrate-asteria', $options)->assertSuccessful();
+        $this->artisan('products:migrate-asteria', $options)->assertSuccessful();
+
+        $this->assertSame(1, Product::query()->where('sku', 'AST-TEST-WIG-001')->count());
+        $product = Product::query()->where('sku', 'AST-TEST-WIG-001')->first();
+        $this->assertSame(self::MID + 10, (int) $product->id);
+        $this->assertSame(self::MID + 10, (int) $product->migrated_from_asteria_id);
+        $this->assertSame(4, ProductVariant::query()->where('product_id', $product->id)->count());
+    }
+
+    public function test_existing_sku_keeps_its_primary_key(): void
+    {
+        $attributeFamilyId = (int) (DB::table('attribute_families')->value('id') ?? 1);
+        $existing = Product::factory()->create([
+            'sku'                 => 'AST-TEST-WIG-001',
+            'type'                => 'flexible_variant',
+            'attribute_family_id' => $attributeFamilyId,
+        ]);
+        $bagistoId = (int) $existing->id;
+
+        $this->seedMagentoProduct();
+
+        $this->artisan('products:migrate-asteria', [
+            '--connection'     => $this->connection,
+            '--reset-progress' => true,
+            '--no-index'       => true,
+        ])->assertSuccessful();
+
+        $existing->refresh();
+        $this->assertSame($bagistoId, (int) $existing->id);
+        $this->assertSame(self::MID + 10, (int) $existing->migrated_from_asteria_id);
+        $this->assertSame(1, Product::query()->where('sku', 'AST-TEST-WIG-001')->count());
+    }
+
+    public function test_it_skips_non_simple_magento_products(): void
+    {
+        MagentoSchema::seedProduct($this->connection, [
+            'entity_id' => self::MID + 11,
+            'sku'       => 'AST-TEST-CFG-001',
+            'name'      => 'Configurable Wig',
+            'type_id'   => 'configurable',
+            'price'     => '99.00',
+        ]);
+
+        $this->artisan('products:migrate-asteria', [
+            '--connection'     => $this->connection,
+            '--reset-progress' => true,
+            '--no-index'       => true,
+        ])->assertSuccessful();
+
+        $this->assertNull(Product::query()->where('sku', 'AST-TEST-CFG-001')->first());
+        $this->assertNull(Product::query()->where('migrated_from_asteria_id', self::MID + 11)->first());
+    }
+
+    private function seedMagentoProduct(): void
+    {
+        MagentoSchema::seedProduct($this->connection, [
+            'entity_id'         => self::MID + 10,
+            'sku'               => 'AST-TEST-WIG-001',
+            'name'              => 'Lace Front Wig',
+            'price'             => '99.00',
+            'short_description' => 'Nice wig',
+            'description'       => 'A lace front wig',
+            'hair_colorr'       => MagentoSchema::HAIR_COLORR_OPTION_ID,
+            'qty'               => 12,
+            'is_in_stock'       => 1,
+            'images'            => ['/w/i/wig.jpg', '/w/i/wig-2.jpg'],
+            'options'           => [
+                [
+                    'title'      => 'Hair Length',
+                    'type'       => 'drop_down',
+                    'sort_order' => 1,
+                    'values'     => [
+                        ['title' => '12"', 'price' => 0, 'sort_order' => 1],
+                        ['title' => '14"', 'price' => 10, 'sort_order' => 2],
+                    ],
+                ],
+                [
+                    'title'      => 'Hair Color',
+                    'type'       => 'drop_down',
+                    'sort_order' => 2,
+                    'values'     => [
+                        ['title' => 'Natural Black', 'price' => 0, 'sort_order' => 1],
+                        ['title' => 'Brown', 'price' => 5, 'sort_order' => 2],
+                    ],
+                ],
+            ],
+        ]);
+    }
+
+    private function attributeText(int $productId, string $code): ?string
+    {
+        $attributeId = DB::table('attributes')->where('code', $code)->value('id');
+
+        if (! $attributeId) {
+            return null;
+        }
+
+        return DB::table('product_attribute_values')
+            ->where('product_id', $productId)
+            ->where('attribute_id', $attributeId)
+            ->value('text_value');
+    }
+
+    private function attributeFloat(int $productId, string $code): ?float
+    {
+        $attributeId = DB::table('attributes')->where('code', $code)->value('id');
+
+        if (! $attributeId) {
+            return null;
+        }
+
+        $value = DB::table('product_attribute_values')
+            ->where('product_id', $productId)
+            ->where('attribute_id', $attributeId)
+            ->value('float_value');
+
+        return $value === null ? null : (float) $value;
+    }
+
+    private function forgetLeakedAsteriaProducts(): void
+    {
+        $ids = Product::query()
+            ->where('sku', 'like', 'AST-TEST-%')
+            ->pluck('id');
+
+        if ($ids->isEmpty()) {
+            return;
+        }
+
+        ProductVariant::query()->whereIn('product_id', $ids)->delete();
+        $variantIds = ProductVariant::withTrashed()->whereIn('product_id', $ids)->pluck('id');
+        if ($variantIds->isNotEmpty()) {
+            DB::table('product_variant_option_values')->whereIn('product_variant_id', $variantIds)->delete();
+            ProductVariant::withTrashed()->whereIn('id', $variantIds)->forceDelete();
+        }
+        DB::table('product_images')->whereIn('product_id', $ids)->delete();
+        DB::table('product_inventories')->whereIn('product_id', $ids)->delete();
+        DB::table('product_attribute_values')->whereIn('product_id', $ids)->delete();
+        DB::table('product_channels')->whereIn('product_id', $ids)->delete();
+        DB::table('product_categories')->whereIn('product_id', $ids)->delete();
+        Product::query()->whereIn('id', $ids)->delete();
+    }
+}