Success. Updated the following files:
M app/Models/ProductVariant.php

@@ -7,2 +7,3 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
@@ -11,2 +12,3 @@
 use Illuminate\Database\Eloquent\Relations\HasOne;
+use RuntimeException;

@@ -48,2 +50,24 @@
     {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+
+        static::saving(function (ProductVariant $variant): void {
+            $variant->assertSkuIsUniqueForStore();
+        });
+
         static::created(function (ProductVariant $variant): void {
@@ -71,2 +95,35 @@

+    private function assertSkuIsUniqueForStore(): void
+    {
+        $sku = trim((string) $this->sku);
+
+        if ($sku === '') {
+            return;
+        }
+
+        $storeId = Product::withoutGlobalScopes()
+            ->whereKey($this->product_id)
+            ->value('store_id');
+
+        if ($storeId === null) {
+            return;
+        }
+
+        $query = self::withoutGlobalScopes()
+            ->where('sku', $sku)
+            ->whereHas('product', function (Builder $query) use ($storeId): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $storeId);
+            });
+
+        if ($this->exists) {
+            $query->whereKeyNot($this->getKey());
+        }
+
+        if ($query->exists()) {
+            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
+        }
+    }
+
     /**


Output: {"output":"Success. Updated the following files:\nM app/Models/ProductVariant.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Models/ProductOption.php
@@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
@@
     protected $attributes = [
         'position' => 0,
     ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }

     /**
      * @return BelongsTo<Product, $this>
*** End Patch


Success. Updated the following files:
M app/Models/ProductOption.php

@@ -5,2 +5,3 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
@@ -32,2 +33,23 @@

+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
     /**


Output: {"output":"Success. Updated the following files:\nM app/Models/ProductOption.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Models/ProductOptionValue.php
@@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
@@
     protected $attributes = [
         'position' => 0,
     ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('option.product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }

     /**
      * @return BelongsTo<ProductOption, $this>
*** End Patch


Success. Updated the following files:
M app/Models/ProductOptionValue.php

@@ -5,2 +5,3 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
@@ -32,2 +33,23 @@

+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('option.product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
     /**


Output: {"output":"Success. Updated the following files:\nM app/Models/ProductOptionValue.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Models/ProductMedia.php
@@
 use App\Enums\MediaStatus;
 use App\Enums\MediaType;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Support\Facades\Storage;
@@
     protected $attributes = [
         'type' => 'image',
         'position' => 0,
         'status' => 'processing',
     ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+
+        static::deleted(function (ProductMedia $media): void {
+            $disk = Storage::disk('public');
+
+            $disk->delete($media->storage_key);
+            $disk->deleteDirectory("media/{$media->product_id}/{$media->getKey()}");
+        });
+    }

     /**
      * @return BelongsTo<Product, $this>
*** End Patch


Success. Updated the following files:
M app/Models/ProductMedia.php

@@ -7,4 +7,6 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Support\Facades\Storage;

@@ -42,2 +44,30 @@

+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('product', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+
+        static::deleted(function (ProductMedia $media): void {
+            $disk = Storage::disk('public');
+
+            $disk->delete($media->storage_key);
+            $disk->deleteDirectory("media/{$media->product_id}/{$media->getKey()}");
+        });
+    }
+
     /**


Output: {"output":"Success. Updated the following files:\nM app/Models/ProductMedia.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Models/InventoryItem.php
@@
 use App\Enums\InventoryPolicy;
 use App\Models\Concerns\BelongsToStore;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use InvalidArgumentException;
@@
     protected $attributes = [
         'quantity_on_hand' => 0,
         'quantity_reserved' => 0,
         'policy' => 'deny',
     ];
+
+    protected static function booted(): void
+    {
+        static::saving(function (InventoryItem $item): void {
+            $storeId = $item->variantStoreId();
+
+            if ($storeId === null) {
+                return;
+            }
+
+            if (! $item->store_id) {
+                $item->store_id = $storeId;
+
+                return;
+            }
+
+            if ((int) $item->store_id !== $storeId) {
+                throw new InvalidArgumentException('Inventory item store must match the variant product store.');
+            }
+        });
+    }
+
+    private function variantStoreId(): ?int
+    {
+        $variant = ProductVariant::withoutGlobalScopes()
+            ->select(['id', 'product_id'])
+            ->find($this->variant_id);
+
+        if (! $variant instanceof ProductVariant) {
+            return null;
+        }
+
+        $storeId = Product::withoutGlobalScopes()
+            ->whereKey($variant->product_id)
+            ->value('store_id');
+
+        return $storeId === null ? null : (int) $storeId;
+    }

     /**
      * @return BelongsTo<Store, $this>
*** End Patch


Success. Updated the following files:
M app/Models/InventoryItem.php

@@ -9,2 +9,3 @@
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use InvalidArgumentException;

@@ -37,2 +38,40 @@

+    protected static function booted(): void
+    {
+        static::saving(function (InventoryItem $item): void {
+            $storeId = $item->variantStoreId();
+
+            if ($storeId === null) {
+                return;
+            }
+
+            if (! $item->store_id) {
+                $item->store_id = $storeId;
+
+                return;
+            }
+
+            if ((int) $item->store_id !== $storeId) {
+                throw new InvalidArgumentException('Inventory item store must match the variant product store.');
+            }
+        });
+    }
+
+    private function variantStoreId(): ?int
+    {
+        $variant = ProductVariant::withoutGlobalScopes()
+            ->select(['id', 'product_id'])
+            ->find($this->variant_id);
+
+        if (! $variant instanceof ProductVariant) {
+            return null;
+        }
+
+        $storeId = Product::withoutGlobalScopes()
+            ->whereKey($variant->product_id)
+            ->value('store_id');
+
+        return $storeId === null ? null : (int) $storeId;
+    }
+
     /**


Output: {"output":"Success. Updated the following files:\nM app/Models/InventoryItem.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Services/VariantMatrixService.php
@@
     private function ensureDefaultVariant(Product $product): void
     {
-        if ($product->variants()->exists()) {
+        $variants = $product->variants()->with('optionValues')->get();
+
+        if ($variants->isEmpty()) {
+            $variant = $product->variants()->create([
+                'price_amount' => 0,
+                'currency' => $this->store($product)->default_currency,
+                'requires_shipping' => true,
+                'is_default' => true,
+                'position' => 0,
+                'status' => VariantStatus::Active,
+            ]);
+
+            InventoryItem::withoutGlobalScopes()->updateOrCreate(
+                ['variant_id' => $variant->getKey()],
+                [
+                    'store_id' => $product->store_id,
+                    'quantity_on_hand' => 0,
+                    'quantity_reserved' => 0,
+                    'policy' => InventoryPolicy::Deny,
+                ],
+            );
+
             return;
         }

-        $variant = $product->variants()->create([
-            'price_amount' => 0,
-            'currency' => $this->store($product)->default_currency,
-            'requires_shipping' => true,
+        $defaultVariant = $variants->sortBy('position')->first();
+
+        $defaultVariant->optionValues()->detach();
+        $defaultVariant->forceFill([
             'is_default' => true,
             'position' => 0,
             'status' => VariantStatus::Active,
-        ]);
+        ])->save();

         InventoryItem::withoutGlobalScopes()->updateOrCreate(
-            ['variant_id' => $variant->getKey()],
+            ['variant_id' => $defaultVariant->getKey()],
             [
                 'store_id' => $product->store_id,
                 'quantity_on_hand' => 0,
@@
                 'policy' => InventoryPolicy::Deny,
             ],
         );
+
+        foreach ($variants as $variant) {
+            if ($variant->is($defaultVariant)) {
+                continue;
+            }
+
+            if ($this->variantHasOrderLines($variant)) {
+                $variant->optionValues()->detach();
+                $variant->forceFill([
+                    'is_default' => false,
+                    'status' => VariantStatus::Archived,
+                ])->save();
+            } else {
+                $variant->delete();
+            }
+        }
     }
*** End Patch


Success. Updated the following files:
M app/Services/VariantMatrixService.php

@@ -73,3 +73,24 @@
     {
-        if ($product->variants()->exists()) {
+        $variants = $product->variants()->with('optionValues')->get();
+
+        if ($variants->isEmpty()) {
+            $variant = $product->variants()->create([
+                'price_amount' => 0,
+                'currency' => $this->store($product)->default_currency,
+                'requires_shipping' => true,
+                'is_default' => true,
+                'position' => 0,
+                'status' => VariantStatus::Active,
+            ]);
+
+            InventoryItem::withoutGlobalScopes()->updateOrCreate(
+                ['variant_id' => $variant->getKey()],
+                [
+                    'store_id' => $product->store_id,
+                    'quantity_on_hand' => 0,
+                    'quantity_reserved' => 0,
+                    'policy' => InventoryPolicy::Deny,
+                ],
+            );
+
             return;
@@ -77,6 +98,6 @@

-        $variant = $product->variants()->create([
-            'price_amount' => 0,
-            'currency' => $this->store($product)->default_currency,
-            'requires_shipping' => true,
+        $defaultVariant = $variants->sortBy('position')->first();
+
+        $defaultVariant->optionValues()->detach();
+        $defaultVariant->forceFill([
             'is_default' => true,
@@ -84,6 +105,6 @@
             'status' => VariantStatus::Active,
-        ]);
+        ])->save();

         InventoryItem::withoutGlobalScopes()->updateOrCreate(
-            ['variant_id' => $variant->getKey()],
+            ['variant_id' => $defaultVariant->getKey()],
             [
@@ -95,2 +116,18 @@
         );
+
+        foreach ($variants as $variant) {
+            if ($variant->is($defaultVariant)) {
+                continue;
+            }
+
+            if ($this->variantHasOrderLines($variant)) {
+                $variant->optionValues()->detach();
+                $variant->forceFill([
+                    'is_default' => false,
+                    'status' => VariantStatus::Archived,
+                ])->save();
+            } else {
+                $variant->delete();
+            }
+        }
     }


Output: {"output":"Success. Updated the following files:\nM app/Services/VariantMatrixService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Services/VariantMatrixService.php
@@
-            InventoryItem::withoutGlobalScopes()->updateOrCreate(
+            InventoryItem::withoutGlobalScopes()->firstOrCreate(
                 ['variant_id' => $variant->getKey()],
                 [
                     'store_id' => $product->store_id,
@@
-        InventoryItem::withoutGlobalScopes()->updateOrCreate(
+        InventoryItem::withoutGlobalScopes()->firstOrCreate(
             ['variant_id' => $defaultVariant->getKey()],
             [
                 'store_id' => $product->store_id,
*** End Patch


@@ -85,3 +85,3 @@

-            InventoryItem::withoutGlobalScopes()->updateOrCreate(
+            InventoryItem::withoutGlobalScopes()->firstOrCreate(
                 ['variant_id' => $variant->getKey()],
@@ -107,3 +107,3 @@

-        InventoryItem::withoutGlobalScopes()->updateOrCreate(
+        InventoryItem::withoutGlobalScopes()->firstOrCreate(
             ['variant_id' => $defaultVariant->getKey()],


Output: {"output":"Success. Updated the following files:\nM app/Services/VariantMatrixService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


I’m also filling in the media job enough for the Phase 2 contract: validate the original, generate predictable resized files with GD, update metadata, use three attempts, and clean files on deletion. It still avoids new dependencies.


Input:
*** Begin Patch
*** Update File: app/Jobs/ProcessMediaUpload.php
@@
 use App\Enums\MediaStatus;
+use App\Enums\MediaType;
 use App\Models\ProductMedia;
 use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Queue\Queueable;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
+use RuntimeException;
 use Throwable;

 class ProcessMediaUpload implements ShouldQueue
 {
     use Queueable;

+    public int $tries = 3;
+
+    /**
+     * @var array<string, array{0: int, 1: int}>
+     */
+    private const TARGETS = [
+        'thumbnail' => [150, 150],
+        'small' => [300, 300],
+        'medium' => [600, 600],
+        'large' => [1200, 1200],
+    ];
+
     public function __construct(
         public int $productMediaId,
+        public ?int $storeId = null,
     ) {}

     public function handle(): void
     {
-        $media = ProductMedia::query()->findOrFail($this->productMediaId);
+        $media = $this->media();

         try {
-            if (! Storage::disk('public')->exists($media->storage_key)) {
-                throw new \RuntimeException('Media file is missing from public storage.');
-            }
-
-            $media->forceFill([
-                'status' => MediaStatus::Ready,
-            ])->save();
+            $this->process($media);
         } catch (Throwable $throwable) {
             $media->forceFill([
                 'status' => MediaStatus::Failed,
             ])->save();

+            Log::error('Media processing failed.', [
+                'product_media_id' => $media->getKey(),
+                'storage_key' => $media->storage_key,
+                'exception' => $throwable->getMessage(),
+            ]);
+
             throw $throwable;
         }
     }
+
+    private function media(): ProductMedia
+    {
+        $query = ProductMedia::withoutGlobalScopes()->whereKey($this->productMediaId);
+
+        if ($this->storeId !== null) {
+            $query->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->storeId);
+            });
+        }
+
+        return $query->firstOrFail();
+    }
+
+    private function process(ProductMedia $media): void
+    {
+        $disk = Storage::disk('public');
+
+        if (! $disk->exists($media->storage_key)) {
+            throw new RuntimeException('Media file is missing from public storage.');
+        }
+
+        $contents = $disk->get($media->storage_key);
+        $mimeType = $this->mimeType($contents);
+        $byteSize = $disk->size($media->storage_key);
+
+        $dimensions = match ($media->type) {
+            MediaType::Image => $this->processImage($media, $contents, $mimeType),
+            MediaType::Video => $this->validateVideo($mimeType),
+        };
+
+        $media->forceFill([
+            'width' => $dimensions['width'],
+            'height' => $dimensions['height'],
+            'mime_type' => $mimeType,
+            'byte_size' => $byteSize,
+            'status' => MediaStatus::Ready,
+        ])->save();
+    }
+
+    /**
+     * @return array{width: int, height: int}
+     */
+    private function processImage(ProductMedia $media, string $contents, string $mimeType): array
+    {
+        if (! str_starts_with($mimeType, 'image/')) {
+            throw new RuntimeException('Media file is not a supported image.');
+        }
+
+        $source = @imagecreatefromstring($contents);
+
+        if ($source === false) {
+            throw new RuntimeException('Media image could not be decoded.');
+        }
+
+        $sourceWidth = imagesx($source);
+        $sourceHeight = imagesy($source);
+        $extension = $this->extensionForMime($mimeType);
+        $disk = Storage::disk('public');
+
+        foreach (self::TARGETS as $size => [$maxWidth, $maxHeight]) {
+            [$targetWidth, $targetHeight] = $this->fitDimensions($sourceWidth, $sourceHeight, $maxWidth, $maxHeight);
+            $resized = imagecreatetruecolor($targetWidth, $targetHeight);
+
+            imagealphablending($resized, false);
+            imagesavealpha($resized, true);
+            imagefilledrectangle(
+                $resized,
+                0,
+                0,
+                $targetWidth,
+                $targetHeight,
+                imagecolorallocatealpha($resized, 0, 0, 0, 127),
+            );
+            imagecopyresampled($resized, $source, 0, 0, 0, 0, $targetWidth, $targetHeight, $sourceWidth, $sourceHeight);
+
+            $basePath = "media/{$media->product_id}/{$media->getKey()}/{$size}";
+
+            $disk->put("{$basePath}.{$extension}", $this->encodeImage($resized, $extension));
+
+            if (function_exists('imagewebp')) {
+                $disk->put("{$basePath}.webp", $this->encodeImage($resized, 'webp'));
+            }
+
+            imagedestroy($resized);
+        }
+
+        imagedestroy($source);
+
+        return [
+            'width' => $sourceWidth,
+            'height' => $sourceHeight,
+        ];
+    }
+
+    /**
+     * @return array{width: null, height: null}
+     */
+    private function validateVideo(string $mimeType): array
+    {
+        if (! str_starts_with($mimeType, 'video/')) {
+            throw new RuntimeException('Media file is not a supported video.');
+        }
+
+        return [
+            'width' => null,
+            'height' => null,
+        ];
+    }
+
+    /**
+     * @return array{0: int, 1: int}
+     */
+    private function fitDimensions(int $sourceWidth, int $sourceHeight, int $maxWidth, int $maxHeight): array
+    {
+        $ratio = min($maxWidth / $sourceWidth, $maxHeight / $sourceHeight, 1);
+
+        return [
+            max(1, (int) round($sourceWidth * $ratio)),
+            max(1, (int) round($sourceHeight * $ratio)),
+        ];
+    }
+
+    private function encodeImage(\GdImage $image, string $extension): string
+    {
+        ob_start();
+
+        $encoded = match ($extension) {
+            'jpg' => imagejpeg($image, null, 90),
+            'png' => imagepng($image),
+            'gif' => imagegif($image),
+            'webp' => imagewebp($image, null, 90),
+            default => false,
+        };
+
+        $contents = ob_get_clean();
+
+        if ($encoded === false || $contents === false) {
+            throw new RuntimeException('Unable to encode resized media image.');
+        }
+
+        return $contents;
+    }
+
+    private function mimeType(string $contents): string
+    {
+        $mimeType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents);
+
+        if (! is_string($mimeType) || $mimeType === '') {
+            throw new RuntimeException('Unable to determine media MIME type.');
+        }
+
+        return $mimeType;
+    }
+
+    private function extensionForMime(string $mimeType): string
+    {
+        return match ($mimeType) {
+            'image/jpeg' => 'jpg',
+            'image/png' => 'png',
+            'image/gif' => 'gif',
+            'image/webp' => 'webp',
+            default => throw new RuntimeException("Unsupported image MIME type [{$mimeType}]."),
+        };
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Jobs/ProcessMediaUpload.php

@@ -5,6 +5,10 @@
 use App\Enums\MediaStatus;
+use App\Enums\MediaType;
 use App\Models\ProductMedia;
 use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Queue\Queueable;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
+use RuntimeException;
 use Throwable;
@@ -15,4 +19,17 @@

+    public int $tries = 3;
+
+    /**
+     * @var array<string, array{0: int, 1: int}>
+     */
+    private const TARGETS = [
+        'thumbnail' => [150, 150],
+        'small' => [300, 300],
+        'medium' => [600, 600],
+        'large' => [1200, 1200],
+    ];
+
     public function __construct(
         public int $productMediaId,
+        public ?int $storeId = null,
     ) {}
@@ -21,12 +38,6 @@
     {
-        $media = ProductMedia::query()->findOrFail($this->productMediaId);
+        $media = $this->media();

         try {
-            if (! Storage::disk('public')->exists($media->storage_key)) {
-                throw new \RuntimeException('Media file is missing from public storage.');
-            }
-
-            $media->forceFill([
-                'status' => MediaStatus::Ready,
-            ])->save();
+            $this->process($media);
         } catch (Throwable $throwable) {
@@ -36,2 +47,8 @@

+            Log::error('Media processing failed.', [
+                'product_media_id' => $media->getKey(),
+                'storage_key' => $media->storage_key,
+                'exception' => $throwable->getMessage(),
+            ]);
+
             throw $throwable;
@@ -39,2 +56,169 @@
     }
+
+    private function media(): ProductMedia
+    {
+        $query = ProductMedia::withoutGlobalScopes()->whereKey($this->productMediaId);
+
+        if ($this->storeId !== null) {
+            $query->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->storeId);
+            });
+        }
+
+        return $query->firstOrFail();
+    }
+
+    private function process(ProductMedia $media): void
+    {
+        $disk = Storage::disk('public');
+
+        if (! $disk->exists($media->storage_key)) {
+            throw new RuntimeException('Media file is missing from public storage.');
+        }
+
+        $contents = $disk->get($media->storage_key);
+        $mimeType = $this->mimeType($contents);
+        $byteSize = $disk->size($media->storage_key);
+
+        $dimensions = match ($media->type) {
+            MediaType::Image => $this->processImage($media, $contents, $mimeType),
+            MediaType::Video => $this->validateVideo($mimeType),
+        };
+
+        $media->forceFill([
+            'width' => $dimensions['width'],
+            'height' => $dimensions['height'],
+            'mime_type' => $mimeType,
+            'byte_size' => $byteSize,
+            'status' => MediaStatus::Ready,
+        ])->save();
+    }
+
+    /**
+     * @return array{width: int, height: int}
+     */
+    private function processImage(ProductMedia $media, string $contents, string $mimeType): array
+    {
+        if (! str_starts_with($mimeType, 'image/')) {
+            throw new RuntimeException('Media file is not a supported image.');
+        }
+
+        $source = @imagecreatefromstring($contents);
+
+        if ($source === false) {
+            throw new RuntimeException('Media image could not be decoded.');
+        }
+
+        $sourceWidth = imagesx($source);
+        $sourceHeight = imagesy($source);
+        $extension = $this->extensionForMime($mimeType);
+        $disk = Storage::disk('public');
+
+        foreach (self::TARGETS as $size => [$maxWidth, $maxHeight]) {
+            [$targetWidth, $targetHeight] = $this->fitDimensions($sourceWidth, $sourceHeight, $maxWidth, $maxHeight);
+            $resized = imagecreatetruecolor($targetWidth, $targetHeight);
+
+            imagealphablending($resized, false);
+            imagesavealpha($resized, true);
+            imagefilledrectangle(
+                $resized,
+                0,
+                0,
+                $targetWidth,
+                $targetHeight,
+                imagecolorallocatealpha($resized, 0, 0, 0, 127),
+            );
+            imagecopyresampled($resized, $source, 0, 0, 0, 0, $targetWidth, $targetHeight, $sourceWidth, $sourceHeight);
+
+            $basePath = "media/{$media->product_id}/{$media->getKey()}/{$size}";
+
+            $disk->put("{$basePath}.{$extension}", $this->encodeImage($resized, $extension));
+
+            if (function_exists('imagewebp')) {
+                $disk->put("{$basePath}.webp", $this->encodeImage($resized, 'webp'));
+            }
+
+            imagedestroy($resized);
+        }
+
+        imagedestroy($source);
+
+        return [
+            'width' => $sourceWidth,
+            'height' => $sourceHeight,
+        ];
+    }
+
+    /**
+     * @return array{width: null, height: null}
+     */
+    private function validateVideo(string $mimeType): array
+    {
+        if (! str_starts_with($mimeType, 'video/')) {
+            throw new RuntimeException('Media file is not a supported video.');
+        }
+
+        return [
+            'width' => null,
+            'height' => null,
+        ];
+    }
+
+    /**
+     * @return array{0: int, 1: int}
+     */
+    private function fitDimensions(int $sourceWidth, int $sourceHeight, int $maxWidth, int $maxHeight): array
+    {
+        $ratio = min($maxWidth / $sourceWidth, $maxHeight / $sourceHeight, 1);
+
+        return [
+            max(1, (int) round($sourceWidth * $ratio)),
+            max(1, (int) round($sourceHeight * $ratio)),
+        ];
+    }
+
+    private function encodeImage(\GdImage $image, string $extension): string
+    {
+        ob_start();
+
+        $encoded = match ($extension) {
+            'jpg' => imagejpeg($image, null, 90),
+            'png' => imagepng($image),
+            'gif' => imagegif($image),
+            'webp' => imagewebp($image, null, 90),
+            default => false,
+        };
+
+        $contents = ob_get_clean();
+
+        if ($encoded === false || $contents === false) {
+            throw new RuntimeException('Unable to encode resized media image.');
+        }
+
+        return $contents;
+    }
+
+    private function mimeType(string $contents): string
+    {
+        $mimeType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents);
+
+        if (! is_string($mimeType) || $mimeType === '') {
+            throw new RuntimeException('Unable to determine media MIME type.');
+        }
+
+        return $mimeType;
+    }
+
+    private function extensionForMime(string $mimeType): string
+    {
+        return match ($mimeType) {
+            'image/jpeg' => 'jpg',
+            'image/png' => 'png',
+            'image/gif' => 'gif',
+            'image/webp' => 'webp',
+            default => throw new RuntimeException("Unsupported image MIME type [{$mimeType}]."),
+        };
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Jobs/ProcessMediaUpload.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


php artisan make:test --pest Catalog/MediaProcessingTest --no-interaction


Output:
Chunk ID: 906f00
Wall time: 0.2268 seconds
Process exited with code 0
Original token count: 22
Output:

   INFO  Test [tests/Feature/Catalog/MediaProcessingTest.php] created successfully.


php artisan make:test --pest Catalog/MediaProcessingTest --no-interaction

INFO  Test [tests/Feature/Catalog/MediaProcessingTest.php] created successfully.


Input:
*** Begin Patch
*** Update File: tests/Feature/Catalog/ProductServiceTest.php
@@
 use App\Enums\ProductStatus;
+use App\Enums\VariantStatus;
 use App\Events\ProductStatusChanged;
 use App\Exceptions\InvalidProductTransitionException;
 use App\Models\Product;
+use App\Models\ProductOptionValue;
 use App\Models\ProductVariant;
@@
 test('product service creates products with a default variant and inventory', function () {
@@
         ->and($product->variants->first()->inventoryItem)->not->toBeNull();
 });
+
+test('product service routes requested active status through lifecycle checks', function () {
+    $store = Store::factory()->create(['default_currency' => 'EUR']);
+    app()->instance('current_store', $store);
+
+    expect(fn () => app(ProductService::class)->create($store, [
+        'title' => 'Unpriced Product',
+        'status' => ProductStatus::Active,
+        'price_amount' => 0,
+    ]))->toThrow(InvalidProductTransitionException::class);
+
+    $product = app(ProductService::class)->create($store, [
+        'title' => 'Priced Product',
+        'status' => ProductStatus::Active,
+        'price_amount' => 1000,
+    ]);
+
+    expect($product->status)->toBe(ProductStatus::Active)
+        ->and($product->published_at)->not->toBeNull();
+});
+
+test('product service syncs variant option selections', function () {
+    $store = Store::factory()->create(['default_currency' => 'EUR']);
+    app()->instance('current_store', $store);
+
+    $product = app(ProductService::class)->create($store, [
+        'title' => 'Optioned Product',
+        'options' => [
+            [
+                'name' => 'Size',
+                'position' => 0,
+                'values' => [
+                    ['value' => 'S', 'position' => 0],
+                    ['value' => 'M', 'position' => 1],
+                ],
+            ],
+            [
+                'name' => 'Color',
+                'position' => 1,
+                'values' => [
+                    ['value' => 'Black', 'position' => 0],
+                    ['value' => 'White', 'position' => 1],
+                ],
+            ],
+        ],
+        'variants' => [
+            [
+                'sku' => 'OPT-S-BLK',
+                'price_amount' => 1500,
+                'status' => VariantStatus::Active,
+                'options' => ['Size' => 'S', 'Color' => 'Black'],
+            ],
+        ],
+    ]);
+
+    $variant = $product->variants()->firstOrFail();
+    $values = $variant->optionValues()
+        ->with('option')
+        ->get()
+        ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
+        ->all();
+
+    expect($values)->toBe([
+        'Size' => 'S',
+        'Color' => 'Black',
+    ]);
+});
@@
 test('duplicate non-empty skus are rejected within the same store', function () {
*** End Patch


Success. Updated the following files:
M tests/Feature/Catalog/ProductServiceTest.php

@@ -3,2 +3,3 @@
 use App\Enums\ProductStatus;
+use App\Enums\VariantStatus;
 use App\Events\ProductStatusChanged;
@@ -6,2 +7,3 @@
 use App\Models\Product;
+use App\Models\ProductOptionValue;
 use App\Models\ProductVariant;
@@ -45,2 +47,69 @@

+test('product service routes requested active status through lifecycle checks', function () {
+    $store = Store::factory()->create(['default_currency' => 'EUR']);
+    app()->instance('current_store', $store);
+
+    expect(fn () => app(ProductService::class)->create($store, [
+        'title' => 'Unpriced Product',
+        'status' => ProductStatus::Active,
+        'price_amount' => 0,
+    ]))->toThrow(InvalidProductTransitionException::class);
+
+    $product = app(ProductService::class)->create($store, [
+        'title' => 'Priced Product',
+        'status' => ProductStatus::Active,
+        'price_amount' => 1000,
+    ]);
+
+    expect($product->status)->toBe(ProductStatus::Active)
+        ->and($product->published_at)->not->toBeNull();
+});
+
+test('product service syncs variant option selections', function () {
+    $store = Store::factory()->create(['default_currency' => 'EUR']);
+    app()->instance('current_store', $store);
+
+    $product = app(ProductService::class)->create($store, [
+        'title' => 'Optioned Product',
+        'options' => [
+            [
+                'name' => 'Size',
+                'position' => 0,
+                'values' => [
+                    ['value' => 'S', 'position' => 0],
+                    ['value' => 'M', 'position' => 1],
+                ],
+            ],
+            [
+                'name' => 'Color',
+                'position' => 1,
+                'values' => [
+                    ['value' => 'Black', 'position' => 0],
+                    ['value' => 'White', 'position' => 1],
+                ],
+            ],
+        ],
+        'variants' => [
+            [
+                'sku' => 'OPT-S-BLK',
+                'price_amount' => 1500,
+                'status' => VariantStatus::Active,
+                'options' => ['Size' => 'S', 'Color' => 'Black'],
+            ],
+        ],
+    ]);
+
+    $variant = $product->variants()->firstOrFail();
+    $values = $variant->optionValues()
+        ->with('option')
+        ->get()
+        ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
+        ->all();
+
+    expect($values)->toBe([
+        'Size' => 'S',
+        'Color' => 'Black',
+    ]);
+});
+
 test('product status transitions enforce activation preconditions and dispatch events', function () {


Output: {"output":"Success. Updated the following files:\nM tests/Feature/Catalog/ProductServiceTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: tests/Feature/Catalog/CatalogFoundationTest.php
@@
 use App\Enums\InventoryPolicy;
 use App\Models\Collection;
 use App\Models\InventoryItem;
 use App\Models\Product;
+use App\Models\ProductMedia;
+use App\Models\ProductOption;
+use App\Models\ProductOptionValue;
 use App\Models\ProductVariant;
 use App\Models\Store;
 use Database\Seeders\DatabaseSeeder;
@@
         ->and(Schema::hasColumns('product_variants', ['product_id', 'sku', 'price_amount', 'requires_shipping']))->toBeTrue()
         ->and(Schema::hasColumns('inventory_items', ['store_id', 'variant_id', 'quantity_on_hand', 'quantity_reserved', 'policy']))->toBeTrue()
         ->and(Schema::hasColumns('collections', ['store_id', 'title', 'handle', 'type', 'status']))->toBeTrue()
-        ->and(Product::withoutGlobalScopes()->count())->toBe(20)
-        ->and(Collection::withoutGlobalScopes()->where('handle', 't-shirts')->exists())->toBeTrue()
-        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->count())->toBe(3)
-        ->and(Product::withoutGlobalScopes()->where('handle', 'sold-out-canvas-tote')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('deny')
-        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-wool-scarf')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('continue');
+        ->and(Store::query()->count())->toBe(2)
+        ->and(Product::withoutGlobalScopes()->count())->toBe(25)
+        ->and(ProductVariant::withoutGlobalScopes()->count())->toBe(117)
+        ->and(InventoryItem::withoutGlobalScopes()->count())->toBe(117)
+        ->and(ProductMedia::withoutGlobalScopes()->count())->toBe(0)
+        ->and(Collection::withoutGlobalScopes()->count())->toBe(6)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->withoutGlobalScopes()->count())->toBe(12)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'pro-laptop-15')->first()?->variants()->withoutGlobalScopes()->count())->toBe(3)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'limited-edition-sneakers')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('deny')
+        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-denim-jacket')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('continue');
 });
@@
     expect(Product::query()->pluck('handle')->all())->toBe(['first-store-product']);
 });
+
+test('catalog child models are scoped through their product store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+    $firstProduct = Product::factory()->create(['store_id' => $firstStore->getKey()]);
+    $secondProduct = Product::factory()->create(['store_id' => $secondStore->getKey()]);
+
+    $firstOption = ProductOption::factory()->create(['product_id' => $firstProduct->getKey(), 'position' => 0]);
+    $secondOption = ProductOption::factory()->create(['product_id' => $secondProduct->getKey(), 'position' => 0]);
+    ProductOptionValue::factory()->create(['product_option_id' => $firstOption->getKey(), 'position' => 0]);
+    ProductOptionValue::factory()->create(['product_option_id' => $secondOption->getKey(), 'position' => 0]);
+    ProductVariant::factory()->create(['product_id' => $firstProduct->getKey(), 'sku' => 'FIRST']);
+    ProductVariant::factory()->create(['product_id' => $secondProduct->getKey(), 'sku' => 'SECOND']);
+    ProductMedia::factory()->create(['product_id' => $firstProduct->getKey()]);
+    ProductMedia::factory()->create(['product_id' => $secondProduct->getKey()]);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(ProductOption::query()->pluck('product_id')->all())->toBe([$firstProduct->getKey()])
+        ->and(ProductOptionValue::query()->count())->toBe(1)
+        ->and(ProductVariant::query()->pluck('sku')->all())->toBe(['FIRST'])
+        ->and(ProductMedia::query()->pluck('product_id')->all())->toBe([$firstProduct->getKey()]);
+});

 test('variants create inventory for their product store regardless of resolved store scope', function () {
@@
     expect($inventory)->not->toBeNull()
         ->and($inventory->store_id)->toBe($productStore->getKey())
         ->and($inventory->policy)->toBe(InventoryPolicy::Deny);
 });
+
+test('inventory store must match the variant product store', function () {
+    $variantStore = Store::factory()->create();
+    $otherStore = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $variantStore->getKey()]);
+    $variant = ProductVariant::factory()->create(['product_id' => $product->getKey()]);
+
+    expect(fn () => InventoryItem::withoutGlobalScopes()->create([
+        'store_id' => $otherStore->getKey(),
+        'variant_id' => $variant->getKey(),
+        'quantity_on_hand' => 1,
+        'quantity_reserved' => 0,
+        'policy' => InventoryPolicy::Deny,
+    ]))->toThrow(InvalidArgumentException::class);
+});
+
+test('variant sku uniqueness is enforced for direct model saves per store', function () {
+    $store = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    $secondProduct = Product::factory()->create(['store_id' => $store->getKey()]);
+
+    ProductVariant::factory()->create(['product_id' => $product->getKey(), 'sku' => 'STORE-SKU']);
+
+    expect(fn () => ProductVariant::factory()->create([
+        'product_id' => $secondProduct->getKey(),
+        'sku' => 'STORE-SKU',
+    ]))->toThrow(RuntimeException::class);
+});
*** End Patch


Success. Updated the following files:
M tests/Feature/Catalog/CatalogFoundationTest.php

@@ -6,2 +6,5 @@
 use App\Models\Product;
+use App\Models\ProductMedia;
+use App\Models\ProductOption;
+use App\Models\ProductOptionValue;
 use App\Models\ProductVariant;
@@ -21,7 +24,12 @@
         ->and(Schema::hasColumns('collections', ['store_id', 'title', 'handle', 'type', 'status']))->toBeTrue()
-        ->and(Product::withoutGlobalScopes()->count())->toBe(20)
-        ->and(Collection::withoutGlobalScopes()->where('handle', 't-shirts')->exists())->toBeTrue()
-        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->count())->toBe(3)
-        ->and(Product::withoutGlobalScopes()->where('handle', 'sold-out-canvas-tote')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('deny')
-        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-wool-scarf')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('continue');
+        ->and(Store::query()->count())->toBe(2)
+        ->and(Product::withoutGlobalScopes()->count())->toBe(25)
+        ->and(ProductVariant::withoutGlobalScopes()->count())->toBe(117)
+        ->and(InventoryItem::withoutGlobalScopes()->count())->toBe(117)
+        ->and(ProductMedia::withoutGlobalScopes()->count())->toBe(0)
+        ->and(Collection::withoutGlobalScopes()->count())->toBe(6)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->withoutGlobalScopes()->count())->toBe(12)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'pro-laptop-15')->first()?->variants()->withoutGlobalScopes()->count())->toBe(3)
+        ->and(Product::withoutGlobalScopes()->where('handle', 'limited-edition-sneakers')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('deny')
+        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-denim-jacket')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('continue');
 });
@@ -40,2 +48,25 @@

+test('catalog child models are scoped through their product store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+    $firstProduct = Product::factory()->create(['store_id' => $firstStore->getKey()]);
+    $secondProduct = Product::factory()->create(['store_id' => $secondStore->getKey()]);
+
+    $firstOption = ProductOption::factory()->create(['product_id' => $firstProduct->getKey(), 'position' => 0]);
+    $secondOption = ProductOption::factory()->create(['product_id' => $secondProduct->getKey(), 'position' => 0]);
+    ProductOptionValue::factory()->create(['product_option_id' => $firstOption->getKey(), 'position' => 0]);
+    ProductOptionValue::factory()->create(['product_option_id' => $secondOption->getKey(), 'position' => 0]);
+    ProductVariant::factory()->create(['product_id' => $firstProduct->getKey(), 'sku' => 'FIRST']);
+    ProductVariant::factory()->create(['product_id' => $secondProduct->getKey(), 'sku' => 'SECOND']);
+    ProductMedia::factory()->create(['product_id' => $firstProduct->getKey()]);
+    ProductMedia::factory()->create(['product_id' => $secondProduct->getKey()]);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(ProductOption::query()->pluck('product_id')->all())->toBe([$firstProduct->getKey()])
+        ->and(ProductOptionValue::query()->count())->toBe(1)
+        ->and(ProductVariant::query()->pluck('sku')->all())->toBe(['FIRST'])
+        ->and(ProductMedia::query()->pluck('product_id')->all())->toBe([$firstProduct->getKey()]);
+});
+
 test('variants create inventory for their product store regardless of resolved store scope', function () {
@@ -56 +87,29 @@
 });
+
+test('inventory store must match the variant product store', function () {
+    $variantStore = Store::factory()->create();
+    $otherStore = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $variantStore->getKey()]);
+    $variant = ProductVariant::factory()->create(['product_id' => $product->getKey()]);
+
+    expect(fn () => InventoryItem::withoutGlobalScopes()->create([
+        'store_id' => $otherStore->getKey(),
+        'variant_id' => $variant->getKey(),
+        'quantity_on_hand' => 1,
+        'quantity_reserved' => 0,
+        'policy' => InventoryPolicy::Deny,
+    ]))->toThrow(InvalidArgumentException::class);
+});
+
+test('variant sku uniqueness is enforced for direct model saves per store', function () {
+    $store = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    $secondProduct = Product::factory()->create(['store_id' => $store->getKey()]);
+
+    ProductVariant::factory()->create(['product_id' => $product->getKey(), 'sku' => 'STORE-SKU']);
+
+    expect(fn () => ProductVariant::factory()->create([
+        'product_id' => $secondProduct->getKey(),
+        'sku' => 'STORE-SKU',
+    ]))->toThrow(RuntimeException::class);
+});


Output: {"output":"Success. Updated the following files:\nM tests/Feature/Catalog/CatalogFoundationTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: tests/Feature/Catalog/VariantMatrixServiceTest.php
@@
 test('variant matrix rebuild creates a default variant when there are no options', function () {
@@
     expect($product->variants()->count())->toBe(1)
         ->and($product->variants()->first()->is_default)->toBeTrue();
 });
+
+test('variant matrix rebuild collapses to one default variant after options are removed', function () {
+    $store = Store::factory()->create();
+    app()->instance('current_store', $store);
+
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    ProductVariant::factory()->count(3)->for($product)->create();
+
+    app(VariantMatrixService::class)->rebuildMatrix($product);
+
+    expect($product->variants()->count())->toBe(1)
+        ->and($product->variants()->first()->is_default)->toBeTrue()
+        ->and($product->variants()->first()->inventoryItem)->not->toBeNull();
+});
*** End Patch


Success. Updated the following files:
M tests/Feature/Catalog/VariantMatrixServiceTest.php

@@ -52 +52,15 @@
 });
+
+test('variant matrix rebuild collapses to one default variant after options are removed', function () {
+    $store = Store::factory()->create();
+    app()->instance('current_store', $store);
+
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    ProductVariant::factory()->count(3)->for($product)->create();
+
+    app(VariantMatrixService::class)->rebuildMatrix($product);
+
+    expect($product->variants()->count())->toBe(1)
+        ->and($product->variants()->first()->is_default)->toBeTrue()
+        ->and($product->variants()->first()->inventoryItem)->not->toBeNull();
+});


Output: {"output":"Success. Updated the following files:\nM tests/Feature/Catalog/VariantMatrixServiceTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,160p' tests/Feature/Catalog/MediaProcessingTest.php


sed -n '1,160p' tests/Feature/Catalog/MediaProcessingTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: 019f76
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Input:
*** Begin Patch
*** Delete File: tests/Feature/Catalog/MediaProcessingTest.php
*** End Patch


Success. Updated the following files:
D tests/Feature/Catalog/MediaProcessingTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output: {"output":"Success. Updated the following files:\nD tests/Feature/Catalog/MediaProcessingTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Add File: tests/Feature/Catalog/MediaProcessingTest.php
+<?php
+
+use App\Enums\MediaStatus;
+use App\Jobs\ProcessMediaUpload;
+use App\Models\Product;
+use App\Models\ProductMedia;
+use App\Models\Store;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Storage;
+
+uses(RefreshDatabase::class);
+
+test('media processing generates resized files and cleans them up on delete', function () {
+    Storage::fake('public');
+
+    $store = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    $media = ProductMedia::factory()->processing()->create([
+        'product_id' => $product->getKey(),
+        'storage_key' => 'media/originals/test.png',
+        'width' => null,
+        'height' => null,
+        'mime_type' => null,
+        'byte_size' => null,
+    ]);
+
+    Storage::disk('public')->put($media->storage_key, catalogPngImageContents());
+
+    (new ProcessMediaUpload($media->getKey(), $store->getKey()))->handle();
+
+    $media->refresh();
+
+    expect($media->status)->toBe(MediaStatus::Ready)
+        ->and($media->width)->toBe(20)
+        ->and($media->height)->toBe(10)
+        ->and($media->mime_type)->toBe('image/png')
+        ->and($media->byte_size)->toBeGreaterThan(0);
+
+    foreach (['thumbnail', 'small', 'medium', 'large'] as $size) {
+        Storage::disk('public')->assertExists("media/{$product->getKey()}/{$media->getKey()}/{$size}.png");
+
+        if (function_exists('imagewebp')) {
+            Storage::disk('public')->assertExists("media/{$product->getKey()}/{$media->getKey()}/{$size}.webp");
+        }
+    }
+
+    $media->delete();
+
+    Storage::disk('public')->assertMissing('media/originals/test.png');
+    Storage::disk('public')->assertMissing("media/{$product->getKey()}/{$media->getKey()}/thumbnail.png");
+});
+
+test('media processing marks invalid image uploads as failed', function () {
+    Storage::fake('public');
+
+    $store = Store::factory()->create();
+    $product = Product::factory()->create(['store_id' => $store->getKey()]);
+    $media = ProductMedia::factory()->processing()->create([
+        'product_id' => $product->getKey(),
+        'storage_key' => 'media/originals/not-image.txt',
+    ]);
+
+    Storage::disk('public')->put($media->storage_key, 'not an image');
+
+    expect(fn () => (new ProcessMediaUpload($media->getKey(), $store->getKey()))->handle())
+        ->toThrow(RuntimeException::class);
+
+    expect($media->refresh()->status)->toBe(MediaStatus::Failed);
+});
+
+function catalogPngImageContents(): string
+{
+    $image = imagecreatetruecolor(20, 10);
+    imagefill($image, 0, 0, imagecolorallocate($image, 20, 80, 140));
+
+    ob_start();
+    imagepng($image);
+    $contents = ob_get_clean();
+
+    imagedestroy($image);
+
+    if ($contents === false) {
+        throw new RuntimeException('Unable to create test image.');
+    }
+
+    return $contents;
+}
*** End Patch


Success. Updated the following files:
A tests/Feature/Catalog/MediaProcessingTest.php

<?php

use App\Enums\MediaStatus;
use App\Jobs\ProcessMediaUpload;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\Store;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;

uses(RefreshDatabase::class);

test('media processing generates resized files and cleans them up on delete', function () {
    Storage::fake('public');

    $store = Store::factory()->create();
    $product = Product::factory()->create(['store_id' => $store->getKey()]);
    $media = ProductMedia::factory()->processing()->create([
        'product_id' => $product->getKey(),
        'storage_key' => 'media/originals/test.png',
        'width' => null,
        'height' => null,
        'mime_type' => null,
        'byte_size' => null,
    ]);

    Storage::disk('public')->put($media->storage_key, catalogPngImageContents());

    (new ProcessMediaUpload($media->getKey(), $store->getKey()))->handle();

    $media->refresh();

    expect($media->status)->toBe(MediaStatus::Ready)
        ->and($media->width)->toBe(20)
        ->and($media->height)->toBe(10)
        ->and($media->mime_type)->toBe('image/png')
        ->and($media->byte_size)->toBeGreaterThan(0);

    foreach (['thumbnail', 'small', 'medium', 'large'] as $size) {
        Storage::disk('public')->assertExists("media/{$product->getKey()}/{$media->getKey()}/{$size}.png");

        if (function_exists('imagewebp')) {
            Storage::disk('public')->assertExists("media/{$product->getKey()}/{$media->getKey()}/{$size}.webp");
        }
    }

    $media->delete();

    Storage::disk('public')->assertMissing('media/originals/test.png');
    Storage::disk('public')->assertMissing("media/{$product->getKey()}/{$media->getKey()}/thumbnail.png");
});

test('media processing marks invalid image uploads as failed', function () {
    Storage::fake('public');

    $store = Store::factory()->create();
    $product = Product::factory()->create(['store_id' => $store->getKey()]);
    $media = ProductMedia::factory()->processing()->create([
        'product_id' => $product->getKey(),
        'storage_key' => 'media/originals/not-image.txt',
    ]);

    Storage::disk('public')->put($media->storage_key, 'not an image');

    expect(fn () => (new ProcessMediaUpload($media->getKey(), $store->getKey()))->handle())
        ->toThrow(RuntimeException::class);

    expect($media->refresh()->status)->toBe(MediaStatus::Failed);
});

function catalogPngImageContents(): string
{
    $image = imagecreatetruecolor(20, 10);
    imagefill($image, 0, 0, imagecolorallocate($image, 20, 80, 140));

    ob_start();
    imagepng($image);
    $contents = ob_get_clean();

    imagedestroy($image);

    if ($contents === false) {
        throw new RuntimeException('Unable to create test image.');
    }

    return $contents;
}


Output: {"output":"Success. Updated the following files:\nA tests/Feature/Catalog/MediaProcessingTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


I’m replacing the simplified catalog seed graph with the spec-shaped seed graph: Acme Fashion’s 20 products and 107 variants, Acme Electronics’ 5 isolation products, six collections, and no seeded media. This should make the seed assertions meaningful for later storefront/cart tests.


Input:
*** Begin Patch
*** Update File: database/seeders/StoreSeeder.php
@@
-        Store::query()->updateOrCreate(
-            ['handle' => 'acme-fashion'],
-            [
-                'organization_id' => $organization->getKey(),
-                'name' => 'Acme Fashion',
-                'status' => 'active',
-                'default_currency' => 'EUR',
-                'default_locale' => 'en',
-                'timezone' => 'Europe/Berlin',
-            ],
-        );
+        foreach ([
+            ['handle' => 'acme-fashion', 'name' => 'Acme Fashion'],
+            ['handle' => 'acme-electronics', 'name' => 'Acme Electronics'],
+        ] as $store) {
+            Store::query()->updateOrCreate(
+                ['handle' => $store['handle']],
+                [
+                    'organization_id' => $organization->getKey(),
+                    'name' => $store['name'],
+                    'status' => 'active',
+                    'default_currency' => 'EUR',
+                    'default_locale' => 'en',
+                    'timezone' => 'Europe/Berlin',
+                ],
+            );
+        }
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/StoreSeeder.php

@@ -17,13 +17,18 @@

-        Store::query()->updateOrCreate(
-            ['handle' => 'acme-fashion'],
-            [
-                'organization_id' => $organization->getKey(),
-                'name' => 'Acme Fashion',
-                'status' => 'active',
-                'default_currency' => 'EUR',
-                'default_locale' => 'en',
-                'timezone' => 'Europe/Berlin',
-            ],
-        );
+        foreach ([
+            ['handle' => 'acme-fashion', 'name' => 'Acme Fashion'],
+            ['handle' => 'acme-electronics', 'name' => 'Acme Electronics'],
+        ] as $store) {
+            Store::query()->updateOrCreate(
+                ['handle' => $store['handle']],
+                [
+                    'organization_id' => $organization->getKey(),
+                    'name' => $store['name'],
+                    'status' => 'active',
+                    'default_currency' => 'EUR',
+                    'default_locale' => 'en',
+                    'timezone' => 'Europe/Berlin',
+                ],
+            );
+        }
     }


Output: {"output":"Success. Updated the following files:\nM database/seeders/StoreSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: database/seeders/StoreDomainSeeder.php
@@
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
-
-        foreach (['shop.test', 'acme-fashion.test'] as $index => $hostname) {
-            StoreDomain::query()->updateOrCreate(
-                ['hostname' => $hostname],
-                [
-                    'store_id' => $store->getKey(),
-                    'type' => 'storefront',
-                    'is_primary' => $index === 0,
-                    'tls_mode' => 'managed',
-                ],
-            );
+        $domainsByStore = [
+            'acme-fashion' => ['shop.test', 'acme-fashion.test'],
+            'acme-electronics' => ['acme-electronics.test'],
+        ];
+
+        foreach ($domainsByStore as $storeHandle => $hostnames) {
+            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
+
+            foreach ($hostnames as $index => $hostname) {
+                StoreDomain::query()->updateOrCreate(
+                    ['hostname' => $hostname],
+                    [
+                        'store_id' => $store->getKey(),
+                        'type' => 'storefront',
+                        'is_primary' => $index === 0,
+                        'tls_mode' => 'managed',
+                    ],
+                );
+            }
         }
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/StoreDomainSeeder.php

@@ -15,14 +15,21 @@
     {
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+        $domainsByStore = [
+            'acme-fashion' => ['shop.test', 'acme-fashion.test'],
+            'acme-electronics' => ['acme-electronics.test'],
+        ];

-        foreach (['shop.test', 'acme-fashion.test'] as $index => $hostname) {
-            StoreDomain::query()->updateOrCreate(
-                ['hostname' => $hostname],
-                [
-                    'store_id' => $store->getKey(),
-                    'type' => 'storefront',
-                    'is_primary' => $index === 0,
-                    'tls_mode' => 'managed',
-                ],
-            );
+        foreach ($domainsByStore as $storeHandle => $hostnames) {
+            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
+
+            foreach ($hostnames as $index => $hostname) {
+                StoreDomain::query()->updateOrCreate(
+                    ['hostname' => $hostname],
+                    [
+                        'store_id' => $store->getKey(),
+                        'type' => 'storefront',
+                        'is_primary' => $index === 0,
+                        'tls_mode' => 'managed',
+                    ],
+                );
+            }
         }


Output: {"output":"Success. Updated the following files:\nM database/seeders/StoreDomainSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: database/seeders/StoreSettingsSeeder.php
@@
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
-
-        StoreSettings::query()->updateOrCreate(
-            ['store_id' => $store->getKey()],
-            [
-                'settings_json' => [
-                    'announcement' => [
-                        'enabled' => true,
-                        'text' => 'Free shipping on orders over 75.00 EUR',
-                    ],
-                    'checkout' => [
-                        'guest_checkout_enabled' => true,
-                    ],
-                ],
-            ],
-        );
+        Store::query()->each(function (Store $store): void {
+            StoreSettings::query()->updateOrCreate(
+                ['store_id' => $store->getKey()],
+                [
+                    'settings_json' => [
+                        'announcement' => [
+                            'enabled' => true,
+                            'text' => 'Free shipping on orders over 75.00 EUR',
+                        ],
+                        'checkout' => [
+                            'guest_checkout_enabled' => true,
+                        ],
+                    ],
+                ],
+            );
+        });
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/StoreSettingsSeeder.php

@@ -15,18 +15,18 @@
     {
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
-
-        StoreSettings::query()->updateOrCreate(
-            ['store_id' => $store->getKey()],
-            [
-                'settings_json' => [
-                    'announcement' => [
-                        'enabled' => true,
-                        'text' => 'Free shipping on orders over 75.00 EUR',
+        Store::query()->each(function (Store $store): void {
+            StoreSettings::query()->updateOrCreate(
+                ['store_id' => $store->getKey()],
+                [
+                    'settings_json' => [
+                        'announcement' => [
+                            'enabled' => true,
+                            'text' => 'Free shipping on orders over 75.00 EUR',
+                        ],
+                        'checkout' => [
+                            'guest_checkout_enabled' => true,
+                        ],
                     ],
-                    'checkout' => [
-                        'guest_checkout_enabled' => true,
-                    ],
                 ],
-            ],
-        );
+            );
+        });
     }


Output: {"output":"Success. Updated the following files:\nM database/seeders/StoreSettingsSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: database/seeders/StoreUserSeeder.php
@@
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
         $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

-        DB::table('store_users')->updateOrInsert(
-            [
-                'store_id' => $store->getKey(),
-                'user_id' => $user->getKey(),
-            ],
-            [
-                'role' => 'owner',
-                'created_at' => now(),
-            ],
-        );
+        Store::query()->each(function (Store $store) use ($user): void {
+            DB::table('store_users')->updateOrInsert(
+                [
+                    'store_id' => $store->getKey(),
+                    'user_id' => $user->getKey(),
+                ],
+                [
+                    'role' => 'owner',
+                    'created_at' => now(),
+                ],
+            );
+        });
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/StoreUserSeeder.php

@@ -16,15 +16,16 @@
     {
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
         $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

-        DB::table('store_users')->updateOrInsert(
-            [
-                'store_id' => $store->getKey(),
-                'user_id' => $user->getKey(),
-            ],
-            [
-                'role' => 'owner',
-                'created_at' => now(),
-            ],
-        );
+        Store::query()->each(function (Store $store) use ($user): void {
+            DB::table('store_users')->updateOrInsert(
+                [
+                    'store_id' => $store->getKey(),
+                    'user_id' => $user->getKey(),
+                ],
+                [
+                    'role' => 'owner',
+                    'created_at' => now(),
+                ],
+            );
+        });
     }


Output: {"output":"Success. Updated the following files:\nM database/seeders/StoreUserSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: database/seeders/CollectionSeeder.php
@@
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
-
-        $collections = [
-            ['title' => 'T-Shirts', 'handle' => 't-shirts', 'description_html' => '
Everyday cotton essentials.
'],
-            ['title' => 'New Arrivals', 'handle' => 'new-arrivals', 'description_html' => '
Fresh pieces just landed.
'],
-            ['title' => 'Sale', 'handle' => 'sale', 'description_html' => '
Limited-time offers.
'],
-            ['title' => 'Accessories', 'handle' => 'accessories', 'description_html' => '
Finishing touches for every order.
'],
-        ];
-
-        foreach ($collections as $collection) {
-            Collection::query()->updateOrCreate(
-                [
-                    'store_id' => $store->getKey(),
-                    'handle' => $collection['handle'],
-                ],
-                [
-                    'title' => $collection['title'],
-                    'description_html' => $collection['description_html'],
-                    'type' => 'manual',
-                    'status' => 'active',
-                ],
-            );
+        $collectionsByStore = [
+            'acme-fashion' => [
+                ['title' => 'New Arrivals', 'handle' => 'new-arrivals', 'description_html' => '
Discover the latest additions to our store.
'],
+                ['title' => 'T-Shirts', 'handle' => 't-shirts', 'description_html' => '
Premium cotton tees for every occasion.
'],
+                ['title' => 'Pants & Jeans', 'handle' => 'pants-jeans', 'description_html' => '
Find the perfect fit from our denim and trouser range.
'],
+                ['title' => 'Sale', 'handle' => 'sale', 'description_html' => '
Great deals on selected items.
'],
+            ],
+            'acme-electronics' => [
+                ['title' => 'Featured', 'handle' => 'featured', 'description_html' => '
Featured electronics for tenant isolation tests.
'],
+                ['title' => 'Accessories', 'handle' => 'accessories', 'description_html' => '
Electronics accessories.
'],
+            ],
+        ];
+
+        foreach ($collectionsByStore as $storeHandle => $collections) {
+            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
+
+            foreach ($collections as $collection) {
+                Collection::query()->updateOrCreate(
+                    [
+                        'store_id' => $store->getKey(),
+                        'handle' => $collection['handle'],
+                    ],
+                    [
+                        'title' => $collection['title'],
+                        'description_html' => $collection['description_html'],
+                        'type' => 'manual',
+                        'status' => 'active',
+                    ],
+                );
+            }
         }
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/CollectionSeeder.php

@@ -15,24 +15,32 @@
     {
-        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+        $collectionsByStore = [
+            'acme-fashion' => [
+                ['title' => 'New Arrivals', 'handle' => 'new-arrivals', 'description_html' => '
Discover the latest additions to our store.
'],
+                ['title' => 'T-Shirts', 'handle' => 't-shirts', 'description_html' => '
Premium cotton tees for every occasion.
'],
+                ['title' => 'Pants & Jeans', 'handle' => 'pants-jeans', 'description_html' => '
Find the perfect fit from our denim and trouser range.
'],
+                ['title' => 'Sale', 'handle' => 'sale', 'description_html' => '
Great deals on selected items.
'],
+            ],
+            'acme-electronics' => [
+                ['title' => 'Featured', 'handle' => 'featured', 'description_html' => '
Featured electronics for tenant isolation tests.
'],
+                ['title' => 'Accessories', 'handle' => 'accessories', 'description_html' => '
Electronics accessories.
'],
+            ],
+        ];

-        $collections = [
-            ['title' => 'T-Shirts', 'handle' => 't-shirts', 'description_html' => '
Everyday cotton essentials.
'],
-            ['title' => 'New Arrivals', 'handle' => 'new-arrivals', 'description_html' => '
Fresh pieces just landed.
'],
-            ['title' => 'Sale', 'handle' => 'sale', 'description_html' => '
Limited-time offers.
'],
-            ['title' => 'Accessories', 'handle' => 'accessories', 'description_html' => '
Finishing touches for every order.
'],
-        ];
+        foreach ($collectionsByStore as $storeHandle => $collections) {
+            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();

-        foreach ($collections as $collection) {
-            Collection::query()->updateOrCreate(
-                [
-                    'store_id' => $store->getKey(),
-                    'handle' => $collection['handle'],
-                ],
-                [
-                    'title' => $collection['title'],
-                    'description_html' => $collection['description_html'],
-                    'type' => 'manual',
-                    'status' => 'active',
-                ],
-            );
+            foreach ($collections as $collection) {
+                Collection::query()->updateOrCreate(
+                    [
+                        'store_id' => $store->getKey(),
+                        'handle' => $collection['handle'],
+                    ],
+                    [
+                        'title' => $collection['title'],
+                        'description_html' => $collection['description_html'],
+                        'type' => 'manual',
+                        'status' => 'active',
+                    ],
+                );
+            }
         }


Output: {"output":"Success. Updated the following files:\nM database/seeders/CollectionSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Delete File: database/seeders/ProductSeeder.php
*** End Patch


Success. Updated the following files:
D database/seeders/ProductSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
        $collections = ProductCollection::query()
            ->where('store_id', $store->getKey())
            ->get()
            ->keyBy('handle');

        foreach ($this->products() as $index => $data) {
            $product = Product::query()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'handle' => $data['handle'],
                ],
                [
                    ...Arr::only($data, ['title', 'status', 'description_html', 'vendor', 'product_type', 'tags', 'published_at']),
                    'store_id' => $store->getKey(),
                ],
            );

            $this->createOptions($product, $data['options'] ?? []);
            $this->createVariants($store, $product, $data['variants']);
            $this->createMedia($product);

            $collectionIds = collect($data['collections'] ?? [])
                ->map(fn (string $handle): ?int => $collections->get($handle)?->getKey())
                ->filter()
                ->mapWithKeys(fn (int $collectionId): array => [$collectionId => ['position' => $index]])
                ->all();

            $product->collections()->syncWithoutDetaching($collectionIds);
        }
    }

    /**
     * @param  array<string, array<int, string>>  $options
     */
    private function createOptions(Product $product, array $options): void
    {
        foreach (array_values($options) as $optionPosition => $valuesByName) {
            $option = ProductOption::query()->updateOrCreate(
                [
                    'product_id' => $product->getKey(),
                    'position' => $optionPosition,
                ],
                [
                    'name' => array_key_first($valuesByName),
                ],
            );

            foreach (array_values($valuesByName[array_key_first($valuesByName)]) as $valuePosition => $value) {
                ProductOptionValue::query()->updateOrCreate(
                    [
                        'product_option_id' => $option->getKey(),
                        'position' => $valuePosition,
                    ],
                    ['value' => $value],
                );
            }
        }
    }

    /**
     * @param  array<int, array<string, mixed>>  $variants
     */
    private function createVariants(Store $store, Product $product, array $variants): void
    {
        foreach ($variants as $position => $variantData) {
            $variant = ProductVariant::query()->updateOrCreate(
                [
                    'product_id' => $product->getKey(),
                    'position' => $position,
                ],
                [
                    'sku' => $variantData['sku'] ?? null,
                    'barcode' => $variantData['barcode'] ?? null,
                    'price_amount' => $variantData['price_amount'],
                    'compare_at_amount' => $variantData['compare_at_amount'] ?? null,
                    'currency' => $store->default_currency,
                    'weight_g' => $variantData['weight_g'] ?? 250,
                    'requires_shipping' => $variantData['requires_shipping'] ?? true,
                    'is_default' => $position === 0,
                    'status' => $variantData['status'] ?? 'active',
                ],
            );

            InventoryItem::query()->updateOrCreate(
                ['variant_id' => $variant->getKey()],
                [
                    'store_id' => $store->getKey(),
                    'quantity_on_hand' => $variantData['quantity_on_hand'] ?? 50,
                    'quantity_reserved' => 0,
                    'policy' => $variantData['policy'] ?? 'deny',
                ],
            );

            if (isset($variantData['options'])) {
                $this->syncVariantOptions($variant, $variantData['options']);
            }
        }
    }

    /**
     * @param  array<string, string>  $selectedOptions
     */
    private function syncVariantOptions(ProductVariant $variant, array $selectedOptions): void
    {
        $valueIds = ProductOptionValue::query()
            ->whereHas('option', function ($query) use ($variant, $selectedOptions): void {
                $query
                    ->where('product_id', $variant->product_id)
                    ->whereIn('name', array_keys($selectedOptions));
            })
            ->get()
            ->filter(fn (ProductOptionValue $value): bool => $selectedOptions[$value->option->name] === $value->value)
            ->pluck('id')
            ->all();

        $variant->optionValues()->sync($valueIds);
    }

    private function createMedia(Product $product): void
    {
        ProductMedia::query()->updateOrCreate(
            [
                'product_id' => $product->getKey(),
                'position' => 0,
            ],
            [
                'type' => 'image',
                'storage_key' => 'products/'.$product->handle.'.jpg',
                'alt_text' => $product->title,
                'width' => 1200,
                'height' => 1200,
                'mime_type' => 'image/jpeg',
                'byte_size' => 180000,
                'status' => 'ready',
            ],
        );
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function products(): array
    {
        $base = [
            [
                'title' => 'Classic Cotton T-Shirt',
                'handle' => 'classic-cotton-t-shirt',
                'product_type' => 'Shirts',
                'tags' => ['new', 'popular'],
                'collections' => ['t-shirts', 'new-arrivals'],
                'options' => [
                    ['Size' => ['S', 'M', 'L', 'XL']],
                    ['Color' => ['Black', 'White', 'Navy']],
                ],
                'variants' => [
                    ['sku' => 'TSH-BLK-M', 'price_amount' => 2499, 'quantity_on_hand' => 50, 'options' => ['Size' => 'M', 'Color' => 'Black']],
                    ['sku' => 'TSH-WHT-M', 'price_amount' => 2499, 'quantity_on_hand' => 50, 'options' => ['Size' => 'M', 'Color' => 'White']],
                    ['sku' => 'TSH-NVY-M', 'price_amount' => 2499, 'quantity_on_hand' => 50, 'options' => ['Size' => 'M', 'Color' => 'Navy']],
                ],
            ],
            [
                'title' => 'Premium Slim Fit Jeans',
                'handle' => 'premium-slim-fit-jeans',
                'product_type' => 'Pants',
                'tags' => ['sale', 'popular'],
                'collections' => ['new-arrivals', 'sale'],
                'variants' => [
                    ['sku' => 'JNS-SLM-32', 'price_amount' => 7999, 'compare_at_amount' => 9999, 'quantity_on_hand' => 35],
                ],
            ],
        ];

        for ($i = 3; $i <= 20; $i++) {
            $base[] = [
                'title' => match ($i) {
                    15 => 'Draft Linen Overshirt',
                    17 => 'Sold Out Canvas Tote',
                    18 => 'Backorder Wool Scarf',
                    default => "Acme Catalog Product {$i}",
                },
                'handle' => match ($i) {
                    15 => 'draft-linen-overshirt',
                    17 => 'sold-out-canvas-tote',
                    18 => 'backorder-wool-scarf',
                    default => "acme-catalog-product-{$i}",
                },
                'status' => $i === 15 ? 'draft' : 'active',
                'published_at' => $i === 15 ? null : now(),
                'product_type' => $i % 2 === 0 ? 'Accessories' : 'Shirts',
                'tags' => $i % 2 === 0 ? ['new'] : ['trending'],
                'collections' => $i % 2 === 0 ? ['accessories'] : ['new-arrivals'],
                'variants' => [[
                    'sku' => "ACME-{$i}",
                    'price_amount' => $i === 17 ? 1999 : 2999 + ($i * 100),
                    'quantity_on_hand' => in_array($i, [17, 18], true) ? 0 : 20 + $i,
                    'policy' => $i === 18 ? 'continue' : 'deny',
                ]],
            ];
        }

        return array_map(function (array $product): array {
            return [
                'status' => 'active',
                'description_html' => '
Built for the Acme Fashion demo catalog.
',
                'vendor' => 'Acme',
                'published_at' => now(),
                ...$product,
            ];
        }, $base);
    }
}


Output: {"output":"Success. Updated the following files:\nD database/seeders/ProductSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}