sed -n '1,260p' app/Http/Resources/Admin/V1/ProductResource.php

<?php

namespace App\Http\Resources\Admin\V1;

use App\Models\ProductMedia;
use App\Models\ProductVariant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;

class ProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'title' => $this->title,
            'handle' => $this->handle,
            'description_html' => $this->when($this->relationLoaded('options'), $this->description_html),
            'status' => $this->status?->value,
            'vendor' => $this->vendor,
            'product_type' => $this->product_type,
            'tags' => $this->tags ?? [],
            'variants_count' => $this->whenCounted('variants'),
            'total_inventory' => $this->when($this->relationLoaded('variants'), fn (): int => $this->variants->sum(
                fn (ProductVariant $variant): int => $variant->inventoryItem?->quantity_on_hand ?? 0,
            )),
            'featured_image' => $this->when($this->relationLoaded('media'), function (): ?array {
                $media = $this->media->first();

                return $media instanceof ProductMedia ? $this->mediaPayload($media) : null;
            }),
            'options' => $this->whenLoaded('options', fn () => $this->options->map(fn ($option): array => [
                'id' => $option->id,
                'name' => $option->name,
                'position' => $option->position,
                'values' => $option->values->map(fn ($value): array => [
                    'id' => $value->id,
                    'value' => $value->value,
                    'position' => $value->position,
                ])->values(),
            ])->values()),
            'variants' => $this->when(
                $this->relationLoaded('variants') && $this->relationLoaded('options'),
                fn () => $this->variants->map(fn (ProductVariant $variant): array => [
                    'id' => $variant->id,
                    'sku' => $variant->sku,
                    'barcode' => $variant->barcode,
                    'price_amount' => $variant->price_amount,
                    'compare_at_amount' => $variant->compare_at_amount,
                    'currency' => $variant->currency,
                    'weight_g' => $variant->weight_g,
                    'requires_shipping' => $variant->requires_shipping,
                    'is_default' => $variant->is_default,
                    'position' => $variant->position,
                    'status' => $variant->status?->value,
                    'option_values' => $variant->optionValues->map(fn ($value): array => [
                        'option_name' => $value->option?->name,
                        'value' => $value->value,
                    ])->values(),
                    'inventory' => [
                        'quantity_on_hand' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                        'quantity_reserved' => $variant->inventoryItem?->quantity_reserved ?? 0,
                        'policy' => $variant->inventoryItem?->policy?->value,
                    ],
                ])->values(),
            ),
            'media' => $this->whenLoaded('media', fn () => $this->media->map(fn (ProductMedia $media): array => $this->mediaPayload($media))->values()),
            'collections' => $this->whenLoaded('collections', fn () => $this->collections->map(fn ($collection): array => [
                'id' => $collection->id,
                'title' => $collection->title,
                'handle' => $collection->handle,
            ])->values()),
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function mediaPayload(ProductMedia $media): array
    {
        return [
            'id' => $media->id,
            'type' => $media->type?->value,
            'storage_key' => $media->storage_key,
            'url' => Storage::disk('public')->url($media->storage_key),
            'alt_text' => $media->alt_text,
            'width' => $media->width,
            'height' => $media->height,
            'mime_type' => $media->mime_type,
            'byte_size' => $media->byte_size,
            'position' => $media->position,
            'status' => $media->status?->value,
        ];
    }
}


Output:
Chunk ID: c5a52e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 983
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\ProductResource;
use App\Models\Product;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\Rule;

class ProductController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in(['draft', 'active', 'archived'])],
            'query' => ['nullable', 'string', 'max:255'],
            'collection_id' => ['nullable', 'integer'],
            'sort' => ['nullable', Rule::in(['title_asc', 'title_desc', 'created_at_asc', 'created_at_desc', 'updated_at_desc'])],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $query = Product::withoutGlobalScopes()
            ->with([
                'media',
                'variants.inventoryItem',
            ])
            ->withCount('variants')
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'collection_id'), function (Builder $query, int $collectionId): void {
                $query->whereHas('collections', fn (Builder $query) => $query->withoutGlobalScopes()->whereKey($collectionId));
            })
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $like = '%'.$search.'%';

                $query->where(function (Builder $query) use ($like): void {
                    $query
                        ->where('title', 'like', $like)
                        ->orWhere('vendor', 'like', $like)
                        ->orWhereHas('variants', fn (Builder $query) => $query->withoutGlobalScopes()->where('sku', 'like', $like));
                });
            });

        $this->applySort($query, (string) data_get($validated, 'sort', 'updated_at_desc'));

        return ProductResource::collection($query->paginate((int) data_get($validated, 'per_page', 25)));
    }

    public function show(Request $request, Store $store, Product $product): ProductResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessProductBelongsToStore($product, $store);

        return ProductResource::make($this->loadProduct($product));
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function abortUnlessProductBelongsToStore(Product $product, Store $store): void
    {
        abort_unless((int) $product->store_id === $store->getKey(), 404);
    }

    private function loadProduct(Product $product): Product
    {
        return $product->load([
            'collections',
            'media',
            'options.values',
            'variants.inventoryItem',
            'variants.optionValues.option',
        ])->loadCount('variants');
    }

    private function applySort(Builder $query, string $sort): void
    {
        match ($sort) {
            'title_asc' => $query->orderBy('title')->orderBy('id'),
            'title_desc' => $query->orderByDesc('title')->orderByDesc('id'),
            'created_at_asc' => $query->orderBy('created_at')->orderBy('id'),
            'created_at_desc' => $query->orderByDesc('created_at')->orderByDesc('id'),
            default => $query->orderByDesc('updated_at')->orderByDesc('id'),
        };
    }
}


Output:
Chunk ID: 4c062a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1179
Output:
<?php

namespace App\Http\Resources\Admin\V1;

use App\Models\ProductMedia;
use App\Models\ProductVariant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;

class ProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'title' => $this->title,
            'handle' => $this->handle,
            'description_html' => $this->when($this->relationLoaded('options'), $this->description_html),
            'status' => $this->status?->value,
            'vendor' => $this->vendor,
            'product_type' => $this->product_type,
            'tags' => $this->tags ?? [],
            'variants_count' => $this->whenCounted('variants'),
            'total_inventory' => $this->when($this->relationLoaded('variants'), fn (): int => $this->variants->sum(
                fn (ProductVariant $variant): int => $variant->inventoryItem?->quantity_on_hand ?? 0,
            )),
            'featured_image' => $this->when($this->relationLoaded('media'), function (): ?array {
                $media = $this->media->first();

                return $media instanceof ProductMedia ? $this->mediaPayload($media) : null;
            }),
            'options' => $this->whenLoaded('options', fn () => $this->options->map(fn ($option): array => [
                'id' => $option->id,
                'name' => $option->name,
                'position' => $option->position,
                'values' => $option->values->map(fn ($value): array => [
                    'id' => $value->id,
                    'value' => $value->value,
                    'position' => $value->position,
                ])->values(),
            ])->values()),
            'variants' => $this->when(
                $this->relationLoaded('variants') && $this->relationLoaded('options'),
                fn () => $this->variants->map(fn (ProductVariant $variant): array => [
                    'id' => $variant->id,
                    'sku' => $variant->sku,
                    'barcode' => $variant->barcode,
                    'price_amount' => $variant->price_amount,
                    'compare_at_amount' => $variant->compare_at_amount,
                    'currency' => $variant->currency,
                    'weight_g' => $variant->weight_g,
                    'requires_shipping' => $variant->requires_shipping,
                    'is_default' => $variant->is_default,
                    'position' => $variant->position,
                    'status' => $variant->status?->value,
                    'option_values' => $variant->optionValues->map(fn ($value): array => [
                        'option_name' => $value->option?->name,
                        'value' => $value->value,
                    ])->values(),
                    'inventory' => [
                        'quantity_on_hand' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                        'quantity_reserved' => $variant->inventoryItem?->quantity_reserved ?? 0,
                        'policy' => $variant->inventoryItem?->policy?->value,
                    ],
                ])->values(),
            ),
            'media' => $this->whenLoaded('media', fn () => $this->media->map(fn (ProductMedia $media): array => $this->mediaPayload($media))->values()),
            'collections' => $this->whenLoaded('collections', fn () => $this->collections->map(fn ($collection): array => [
                'id' => $collection->id,
                'title' => $collection->title,
                'handle' => $collection->handle,
            ])->values()),
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function mediaPayload(ProductMedia $media): array
    {
        return [
            'id' => $media->id,
            'type' => $media->type?->value,
            'storage_key' => $media->storage_key,
            'url' => Storage::disk('public')->url($media->storage_key),
            'alt_text' => $media->alt_text,
            'width' => $media->width,
            'height' => $media->height,
            'mime_type' => $media->mime_type,
            'byte_size' => $media->byte_size,
            'position' => $media->position,
            'status' => $media->status?->value,
        ];
    }
}


sed -n '1,280p' tests/Feature/Api/AdminCatalogApiTest.php

AdminCatalogApiTest.php

<?php

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminCatalogApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminCatalogApiUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminCatalogApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Catalog integration', $abilities);
}

test('admin product api lists and shows store scoped products', function (): void {
    $store = adminCatalogApiStore();
    $product = Product::factory()
        ->withDefaultVariant(3299)
        ->create([
            'store_id' => $store->getKey(),
            'title' => 'Admin API Jacket',
            'handle' => 'admin-api-jacket',
            'vendor' => 'Catalog Test Vendor',
            'tags' => ['outerwear', 'api'],
        ]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();
    Product::factory()
        ->withDefaultVariant()
        ->create([
            'store_id' => Store::factory()->create()->getKey(),
            'title' => 'Other Store Jacket',
        ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/products")
        ->assertUnauthorized();

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products?query={$variant->sku}&sort=title_asc")
        ->assertOk()
        ->assertJsonPath('data.0.title', 'Admin API Jacket')
        ->assertJsonPath('data.0.variants_count', 1)
        ->assertJsonPath('data.0.total_inventory', 50)
        ->assertJsonMissing(['title' => 'Other Store Jacket']);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products/{$product->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.title', 'Admin API Jacket')
        ->assertJsonPath('data.variants.0.price_amount', 3299)
        ->assertJsonPath('data.variants.0.inventory.quantity_on_hand', 50);
});

test('admin customer api lists and shows store scoped customers', function (): void {
    $store = adminCatalogApiStore();
    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer-api@example.test',
        'name' => 'Customer API',
        'marketing_opt_in' => true,
    ]);
    CustomerAddress::factory()->default()->create(['customer_id' => $customer->getKey()]);
    Order::factory()->paid()->forCustomer($customer)->create([
        'store_id' => $store->getKey(),
        'order_number' => '#CA-1001',
        'total_amount' => 4400,
    ]);
    Customer::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'email' => 'other-customer@example.test',
    ]);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers?query=customer-api")
        ->assertOk()
        ->assertJsonPath('data.0.email', 'customer-api@example.test')
        ->assertJsonPath('data.0.orders_count', 1)
        ->assertJsonPath('data.0.total_spent_amount', 4400)
        ->assertJsonMissing(['email' => 'other-customer@example.test']);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers/{$customer->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.email', 'customer-api@example.test')
        ->assertJsonPath('data.addresses.0.is_default', true)
        ->assertJsonPath('data.orders.0.order_number', '#CA-1001');
});

test('admin catalog api accepts scoped tokens and enforces abilities', function (): void {
    $store = adminCatalogApiStore();
    Product::factory()->withDefaultVariant()->create([
        'store_id' => $store->getKey(),
        'title' => 'Token Visible Product',
    ]);
    Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'token-customer@example.test',
    ]);
    $productToken = adminCatalogApiToken($store, ['read-products']);
    $customerToken = adminCatalogApiToken($store, ['read-customers']);

    $this->withToken($productToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products?query=Token Visible")
        ->assertOk()
        ->assertJsonPath('data.0.title', 'Token Visible Product');

    $this->withToken($productToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers")
        ->assertForbidden();

    $this->withToken($customerToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers?query=token-customer")
        ->assertOk()
        ->assertJsonPath('data.0.email', 'token-customer@example.test');

    expect($productToken['token']->refresh()->last_used_at)->not->toBeNull()
        ->and($customerToken['token']->refresh()->last_used_at)->not->toBeNull();
});

test('admin catalog api rejects resources outside the requested store', function (): void {
    $store = adminCatalogApiStore();
    $otherStore = Store::factory()->create();
    $otherProduct = Product::factory()->withDefaultVariant()->create(['store_id' => $otherStore->getKey()]);
    $otherCustomer = Customer::factory()->create(['store_id' => $otherStore->getKey()]);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products/{$otherProduct->getKey()}")
        ->assertNotFound();

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers/{$otherCustomer->getKey()}")
        ->assertNotFound();
});

test('deferred oauth and app ecosystem routes return not implemented', function (): void {
    $store = adminCatalogApiStore();

    $this->actingAs(adminCatalogApiUser())
        ->getJson('/oauth/authorize?client_id=test&redirect_uri=https://example.test/callback&response_type=code&scope=read-products&state=state')
        ->assertStatus(501)
        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');

    $this->postJson('/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'test',
        'client_secret' => 'secret',
        'redirect_uri' => 'https://example.test/callback',
        'code' => 'code',
    ])
        ->assertStatus(501)
        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');

    $this->getJson("/api/apps/v1/stores/{$store->getKey()}/products")
        ->assertStatus(501)
        ->assertJsonPath('message', 'App API endpoints are deferred for initial implementation.');
});


sed -n '1,320p' app/Livewire/Admin/Products/Form.php

Form.php

<?php

namespace App\Livewire\Admin\Products;

use App\Enums\MediaStatus;
use App\Enums\MediaType;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Jobs\ProcessMediaUpload;
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 App\Services\ProductService;
use App\Support\Money;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;
use Livewire\WithFileUploads;

class Form extends Component
{
    use WithFileUploads;

    public ?Product $product = null;

    public string $title = '';

    public string $descriptionHtml = '';

    public string $status = 'active';

    public string $vendor = '';

    public string $productType = '';

    public string $tags = '';

    public string $handle = '';

    public ?string $publishedAt = null;

    /**
     * @var array<int, int>
     */
    public array $collectionIds = [];

    /**
     * @var array<int, array{name: string, values: string}>
     */
    public array $options = [];

    /**
     * @var array<int, array{id: int|null, label: string, sku: string, price: string, compareAtPrice: string, quantity: int, requiresShipping: bool, options: array<string, string>}>
     */
    public array $variants = [];

    /**
     * @var array<int, array{id: int, url: string, exists: bool, altText: string, position: int, status: string}>
     */
    public array $media = [];

    /**
     * @var array<int, mixed>
     */
    public array $newMedia = [];

    public function mount(?Product $product = null): void
    {
        if ($product?->exists) {
            $store = app('current_store');

            abort_unless($store instanceof Store && (int) $product->store_id === $store->getKey(), 404);

            $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
            $this->fillFromProduct($this->product);

            return;
        }

        $this->variants = [[
            'id' => null,
            'label' => 'Default',
            'sku' => '',
            'price' => '0.00',
            'compareAtPrice' => '',
            'quantity' => 0,
            'requiresShipping' => true,
            'options' => [],
        ]];
    }

    public function updatedTitle(): void
    {
        if ($this->product === null && $this->handle === '') {
            $this->handle = Str::slug($this->title);
        }
    }

    public function addOption(): void
    {
        $this->options[] = [
            'name' => '',
            'values' => '',
        ];
    }

    public function removeOption(int $index): void
    {
        unset($this->options[$index]);
        $this->options = array_values($this->options);
    }

    public function generateVariants(): void
    {
        $optionPayload = $this->optionPayload();

        if ($optionPayload === []) {
            $template = $this->variants[0] ?? [];
            $this->variants = [[
                'id' => $template['id'] ?? null,
                'label' => 'Default',
                'sku' => $template['sku'] ?? '',
                'price' => $template['price'] ?? '0.00',
                'compareAtPrice' => $template['compareAtPrice'] ?? '',
                'quantity' => (int) ($template['quantity'] ?? 0),
                'requiresShipping' => (bool) ($template['requiresShipping'] ?? true),
                'options' => [],
            ]];

            return;
        }

        $existingVariants = collect($this->variants)
            ->keyBy(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []));
        $template = $this->variants[0] ?? [];

        $this->variants = collect($this->optionCombinations($optionPayload))
            ->map(function (array $options) use ($existingVariants, $template): array {
                $existingVariant = $existingVariants->get($this->variantOptionKey($options));

                return [
                    'id' => $existingVariant['id'] ?? null,
                    'label' => $this->variantLabel($options),
                    'sku' => $existingVariant['sku'] ?? '',
                    'price' => $existingVariant['price'] ?? ($template['price'] ?? '0.00'),
                    'compareAtPrice' => $existingVariant['compareAtPrice'] ?? ($template['compareAtPrice'] ?? ''),
                    'quantity' => (int) ($existingVariant['quantity'] ?? 0),
                    'requiresShipping' => (bool) ($existingVariant['requiresShipping'] ?? ($template['requiresShipping'] ?? true)),
                    'options' => $options,
                ];
            })
            ->values()
            ->all();
    }

    public function save(): void
    {
        $store = …560 chars truncated…              'required',
                'string',
                'max:255',
                Rule::unique('products', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->product?->getKey()),
            ],
            'variants' => ['required', 'array', 'min:1'],
            'variants.*.sku' => ['nullable', 'string', 'max:255'],
            'variants.*.price' => ['required', 'numeric', 'min:0'],
            'variants.*.compareAtPrice' => ['nullable', 'numeric', 'min:0'],
            'variants.*.quantity' => ['required', 'integer', 'min:0'],
            'variants.*.requiresShipping' => ['boolean'],
            'newMedia' => ['array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        if (! $this->validateActiveVariantPricing() || ! $this->validateVariantSkus($store)) {
            return;
        }

        $product = DB::transaction(function () use ($store): Product {
            $productService = app(ProductService::class);

            if ($this->product === null) {
                $product = $productService->create($store, [
                    ...$this->productPayload(),
                    'options' => $this->optionPayload(),
                    'variants' => $this->variantPayload($store),
                ]);
            } else {
                $payload = $this->productPayload();
                $requestedStatus = ProductStatus::from((string) $payload['status']);
                unset($payload['status']);

                $product = $productService->update($this->product, $payload);
                $this->syncProductOptions($product);
                $this->syncExistingVariants($product, $store);

                if ($requestedStatus !== $product->refresh()->status) {
                    $productService->transitionStatus($product, $requestedStatus);
                }
            }

            $product->collections()->sync($this->collectionIds);

            return $product->refresh();
        });

        if ($this->newMedia !== []) {
            $this->storeNewMedia($product);
        }

        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Product saved');
        $this->dispatch('toast', type: 'success', message: __('Product saved'));
    }

    public function uploadMedia(): void
    {
        $product = $this->productForAction();

        $this->validate([
            'newMedia' => ['required', 'array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        $this->storeNewMedia($product);

        $this->product = $product->refresh()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Media uploaded');
        $this->dispatch('toast', type: 'success', message: __('Media uploaded'));
    }

    public function updateMediaAlt(int $mediaId): void
    {
        $index = $this->mediaIndex($mediaId);

        $this->validate([
            "media.{$index}.altText" => ['nullable', 'string', 'max:255'],
        ]);

        $media = $this->mediaRecord($mediaId);
        $altText = trim((string) $this->media[$index]['altText']);

        $media->forceFill([
            'alt_text' => $altText === '' ? null : $altText,
        ])->save();

        $this->refreshProductMedia();

        session()->flash('status', 'Media updated');
    }

    public function moveMedia(int $mediaId, string $direction): void
    {
        $ids = collect($this->media)->pluck('id')->map(fn (int $id): int => $id)->all();
        $index = array_search($mediaId, $ids, true);

        if ($index === false) {
            return;
        }

        $swapIndex = match ($direction) {
            'up' => $index - 1,
            'down' => $index + 1,
            default => $index,
        };

        if (! isset($ids[$swapIndex])) {
            return;
        }

        [$ids[$index], $ids[$swapIndex]] = [$ids[$swapIndex], $ids[$index]];

        $this->reorderMedia($ids);
    }

    /**
     * @param  array<int, int>  $order
     */
    public function reorderMedia(array $order): void
    {
        $product = $this->productForAction();
        $validIds = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->whereIn('id', $order)
            ->pluck('id')
            ->all();

        $orderedIds = collect($order)
            ->map(fn (mixed $mediaId): int => (int) $mediaId)
            ->intersect($validIds)
            ->values();

        DB::transaction(function () use ($orderedIds): void {
            foreach ($orderedIds as $position => $mediaId) {
                ProductMedia::withoutGlobalScopes()


Output:
Chunk ID: 3c52f9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1803
Output:
<?php

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminCatalogApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminCatalogApiUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminCatalogApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Catalog integration', $abilities);
}

test('admin product api lists and shows store scoped products', function (): void {
    $store = adminCatalogApiStore();
    $product = Product::factory()
        ->withDefaultVariant(3299)
        ->create([
            'store_id' => $store->getKey(),
            'title' => 'Admin API Jacket',
            'handle' => 'admin-api-jacket',
            'vendor' => 'Catalog Test Vendor',
            'tags' => ['outerwear', 'api'],
        ]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();
    Product::factory()
        ->withDefaultVariant()
        ->create([
            'store_id' => Store::factory()->create()->getKey(),
            'title' => 'Other Store Jacket',
        ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/products")
        ->assertUnauthorized();

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products?query={$variant->sku}&sort=title_asc")
        ->assertOk()
        ->assertJsonPath('data.0.title', 'Admin API Jacket')
        ->assertJsonPath('data.0.variants_count', 1)
        ->assertJsonPath('data.0.total_inventory', 50)
        ->assertJsonMissing(['title' => 'Other Store Jacket']);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products/{$product->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.title', 'Admin API Jacket')
        ->assertJsonPath('data.variants.0.price_amount', 3299)
        ->assertJsonPath('data.variants.0.inventory.quantity_on_hand', 50);
});

test('admin customer api lists and shows store scoped customers', function (): void {
    $store = adminCatalogApiStore();
    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer-api@example.test',
        'name' => 'Customer API',
        'marketing_opt_in' => true,
    ]);
    CustomerAddress::factory()->default()->create(['customer_id' => $customer->getKey()]);
    Order::factory()->paid()->forCustomer($customer)->create([
        'store_id' => $store->getKey(),
        'order_number' => '#CA-1001',
        'total_amount' => 4400,
    ]);
    Customer::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'email' => 'other-customer@example.test',
    ]);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers?query=customer-api")
        ->assertOk()
        ->assertJsonPath('data.0.email', 'customer-api@example.test')
        ->assertJsonPath('data.0.orders_count', 1)
        ->assertJsonPath('data.0.total_spent_amount', 4400)
        ->assertJsonMissing(['email' => 'other-customer@example.test']);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers/{$customer->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.email', 'customer-api@example.test')
        ->assertJsonPath('data.addresses.0.is_default', true)
        ->assertJsonPath('data.orders.0.order_number', '#CA-1001');
});

test('admin catalog api accepts scoped tokens and enforces abilities', function (): void {
    $store = adminCatalogApiStore();
    Product::factory()->withDefaultVariant()->create([
        'store_id' => $store->getKey(),
        'title' => 'Token Visible Product',
    ]);
    Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'token-customer@example.test',
    ]);
    $productToken = adminCatalogApiToken($store, ['read-products']);
    $customerToken = adminCatalogApiToken($store, ['read-customers']);

    $this->withToken($productToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products?query=Token Visible")
        ->assertOk()
        ->assertJsonPath('data.0.title', 'Token Visible Product');

    $this->withToken($productToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers")
        ->assertForbidden();

    $this->withToken($customerToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers?query=token-customer")
        ->assertOk()
        ->assertJsonPath('data.0.email', 'token-customer@example.test');

    expect($productToken['token']->refresh()->last_used_at)->not->toBeNull()
        ->and($customerToken['token']->refresh()->last_used_at)->not->toBeNull();
});

test('admin catalog api rejects resources outside the requested store', function (): void {
    $store = adminCatalogApiStore();
    $otherStore = Store::factory()->create();
    $otherProduct = Product::factory()->withDefaultVariant()->create(['store_id' => $otherStore->getKey()]);
    $otherCustomer = Customer::factory()->create(['store_id' => $otherStore->getKey()]);

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/products/{$otherProduct->getKey()}")
        ->assertNotFound();

    $this->actingAs(adminCatalogApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/customers/{$otherCustomer->getKey()}")
        ->assertNotFound();
});

test('deferred oauth and app ecosystem routes return not implemented', function (): void {
    $store = adminCatalogApiStore();

    $this->actingAs(adminCatalogApiUser())
        ->getJson('/oauth/authorize?client_id=test&redirect_uri=https://example.test/callback&response_type=code&scope=read-products&state=state')
        ->assertStatus(501)
        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');

    $this->postJson('/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'test',
        'client_secret' => 'secret',
        'redirect_uri' => 'https://example.test/callback',
        'code' => 'code',
    ])
        ->assertStatus(501)
        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');

    $this->getJson("/api/apps/v1/stores/{$store->getKey()}/products")
        ->assertStatus(501)
        ->assertJsonPath('message', 'App API endpoints are deferred for initial implementation.');
});


Output:
Chunk ID: a0dc3b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2640
Output:
<?php

namespace App\Livewire\Admin\Products;

use App\Enums\MediaStatus;
use App\Enums\MediaType;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Jobs\ProcessMediaUpload;
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 App\Services\ProductService;
use App\Support\Money;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;
use Livewire\WithFileUploads;

class Form extends Component
{
    use WithFileUploads;

    public ?Product $product = null;

    public string $title = '';

    public string $descriptionHtml = '';

    public string $status = 'active';

    public string $vendor = '';

    public string $productType = '';

    public string $tags = '';

    public string $handle = '';

    public ?string $publishedAt = null;

    /**
     * @var array<int, int>
     */
    public array $collectionIds = [];

    /**
     * @var array<int, array{name: string, values: string}>
     */
    public array $options = [];

    /**
     * @var array<int, array{id: int|null, label: string, sku: string, price: string, compareAtPrice: string, quantity: int, requiresShipping: bool, options: array<string, string>}>
     */
    public array $variants = [];

    /**
     * @var array<int, array{id: int, url: string, exists: bool, altText: string, position: int, status: string}>
     */
    public array $media = [];

    /**
     * @var array<int, mixed>
     */
    public array $newMedia = [];

    public function mount(?Product $product = null): void
    {
        if ($product?->exists) {
            $store = app('current_store');

            abort_unless($store instanceof Store && (int) $product->store_id === $store->getKey(), 404);

            $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
            $this->fillFromProduct($this->product);

            return;
        }

        $this->variants = [[
            'id' => null,
            'label' => 'Default',
            'sku' => '',
            'price' => '0.00',
            'compareAtPrice' => '',
            'quantity' => 0,
            'requiresShipping' => true,
            'options' => [],
        ]];
    }

    public function updatedTitle(): void
    {
        if ($this->product === null && $this->handle === '') {
            $this->handle = Str::slug($this->title);
        }
    }

    public function addOption(): void
    {
        $this->options[] = [
            'name' => '',
            'values' => '',
        ];
    }

    public function removeOption(int $index): void
    {
        unset($this->options[$index]);
        $this->options = array_values($this->options);
    }

    public function generateVariants(): void
    {
        $optionPayload = $this->optionPayload();

        if ($optionPayload === []) {
            $template = $this->variants[0] ?? [];
            $this->variants = [[
                'id' => $template['id'] ?? null,
                'label' => 'Default',
                'sku' => $template['sku'] ?? '',
                'price' => $template['price'] ?? '0.00',
                'compareAtPrice' => $template['compareAtPrice'] ?? '',
                'quantity' => (int) ($template['quantity'] ?? 0),
                'requiresShipping' => (bool) ($template['requiresShipping'] ?? true),
                'options' => [],
            ]];

            return;
        }

        $existingVariants = collect($this->variants)
            ->keyBy(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []));
        $template = $this->variants[0] ?? [];

        $this->variants = collect($this->optionCombinations($optionPayload))
            ->map(function (array $options) use ($existingVariants, $template): array {
                $existingVariant = $existingVariants->get($this->variantOptionKey($options));

                return [
                    'id' => $existingVariant['id'] ?? null,
                    'label' => $this->variantLabel($options),
                    'sku' => $existingVariant['sku'] ?? '',
                    'price' => $existingVariant['price'] ?? ($template['price'] ?? '0.00'),
                    'compareAtPrice' => $existingVariant['compareAtPrice'] ?? ($template['compareAtPrice'] ?? ''),
                    'quantity' => (int) ($existingVariant['quantity'] ?? 0),
                    'requiresShipping' => (bool) ($existingVariant['requiresShipping'] ?? ($template['requiresShipping'] ?? true)),
                    'options' => $options,
                ];
            })
            ->values()
            ->all();
    }

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->ensureVariantMatrixMatchesOptions();

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
            'vendor' => ['nullable', 'string', 'max:255'],
            'productType' => ['nullable', 'string', 'max:255'],
            'tags' => ['nullable', 'string'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('products', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->product?->getKey()),
            ],
            'variants' => ['required', 'array', 'min:1'],
            'variants.*.sku' => ['nullable', 'string', 'max:255'],
            'variants.*.price' => ['required', 'numeric', 'min:0'],
            'variants.*.compareAtPrice' => ['nullable', 'numeric', 'min:0'],
            'variants.*.quantity' => ['required', 'integer', 'min:0'],
            'variants.*.requiresShipping' => ['boolean'],
            'newMedia' => ['array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        if (! $this->validateActiveVariantPricing() || ! $this->validateVariantSkus($store)) {
            return;
        }

        $product = DB::transaction(function () use ($store): Product {
            $productService = app(ProductService::class);

            if ($this->product === null) {
                $product = $productService->create($store, [
                    ...$this->productPayload(),
                    'options' => $this->optionPayload(),
                    'variants' => $this->variantPayload($store),
                ]);
            } else {
                $payload = $this->productPayload();
                $requestedStatus = ProductStatus::from((string) $payload['status']);
                unset($payload['status']);

                $product = $productService->update($this->product, $payload);
                $this->syncProductOptions($product);
                $this->syncExistingVariants($product, $store);

                if ($requestedStatus !== $product->refresh()->status) {
                    $productService->transitionStatus($product, $requestedStatus);
                }
            }

            $product->collections()->sync($this->collectionIds);

            return $product->refresh();
        });

        if ($this->newMedia !== []) {
            $this->storeNewMedia($product);
        }

        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Product saved');
        $this->dispatch('toast', type: 'success', message: __('Product saved'));
    }

    public function uploadMedia(): void
    {
        $product = $this->productForAction();

        $this->validate([
            'newMedia' => ['required', 'array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        $this->storeNewMedia($product);

        $this->product = $product->refresh()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Media uploaded');
        $this->dispatch('toast', type: 'success', message: __('Media uploaded'));
    }

    public function updateMediaAlt(int $mediaId): void
    {
        $index = $this->mediaIndex($mediaId);

        $this->validate([
            "media.{$index}.altText" => ['nullable', 'string', 'max:255'],
        ]);

        $media = $this->mediaRecord($mediaId);
        $altText = trim((string) $this->media[$index]['altText']);

        $media->forceFill([
            'alt_text' => $altText === '' ? null : $altText,
        ])->save();

        $this->refreshProductMedia();

        session()->flash('status', 'Media updated');
    }

    public function moveMedia(int $mediaId, string $direction): void
    {
        $ids = collect($this->media)->pluck('id')->map(fn (int $id): int => $id)->all();
        $index = array_search($mediaId, $ids, true);

        if ($index === false) {
            return;
        }

        $swapIndex = match ($direction) {
            'up' => $index - 1,
            'down' => $index + 1,
            default => $index,
        };

        if (! isset($ids[$swapIndex])) {
            return;
        }

        [$ids[$index], $ids[$swapIndex]] = [$ids[$swapIndex], $ids[$index]];

        $this->reorderMedia($ids);
    }

    /**
     * @param  array<int, int>  $order
     */
    public function reorderMedia(array $order): void
    {
        $product = $this->productForAction();
        $validIds = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->whereIn('id', $order)
            ->pluck('id')
            ->all();

        $orderedIds = collect($order)
            ->map(fn (mixed $mediaId): int => (int) $mediaId)
            ->intersect($validIds)
            ->values();

        DB::transaction(function () use ($orderedIds): void {
            foreach ($orderedIds as $position => $mediaId) {
                ProductMedia::withoutGlobalScopes()


sed -n '320,760p' app/Livewire/Admin/Products/Form.php


sed -n '1,260p' app/Services/ProductService.php


rg -n "ProductMedia|temporaryUploadUrl|presign|media" app database tests specs/02-API-ROUTES.md specs/03-ADMIN-UI.md specs/07-SEEDERS-AND-TEST-DATA.md -S


sed -n '1,260p' app/Services/ProductService.php

<?php

namespace App\Services;

use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Events\ProductStatusChanged;
use App\Exceptions\InvalidProductTransitionException;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Support\HandleGenerator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use InvalidArgumentException;
use RuntimeException;

class ProductService
{
    public function __construct(
        private readonly HandleGenerator $handles,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(Store $store, array $data): Product
    {
        return DB::transaction(function () use ($store, $data): Product {
            $requestedStatus = $this->requestedProductStatus($data) ?? ProductStatus::Draft;

            $product = Product::query()->create([
                ...Arr::only($data, [
                    'title',
                    'description_html',
                    'vendor',
                    'product_type',
                    'tags',
                ]),
                'store_id' => $store->getKey(),
                'handle' => $data['handle'] ?? $this->handles->generate($data['title'], 'products', $store->getKey()),
                'status' => ProductStatus::Draft,
                'published_at' => null,
            ]);

            foreach ($data['options'] ?? [] as $optionData) {
                $option = $product->options()->create(Arr::only($optionData, ['name', 'position']));

                foreach ($optionData['values'] ?? [] as $valueData) {
                    $option->values()->create(Arr::only($valueData, ['value', 'position']));
                }
            }

            $variants = $data['variants'] ?? [];

            if ($variants === []) {
                $variants = [[
                    'sku' => $data['sku'] ?? null,
                    'price_amount' => $data['price_amount'] ?? 0,
                    'currency' => $store->default_currency,
                    'is_default' => true,
                    'position' => 0,
                ]];
            }

            foreach ($variants as $variantData) {
                $this->createVariant($product, $store, $variantData);
            }

            if ($requestedStatus !== ProductStatus::Draft) {
                $this->transitionStatus($product->refresh(), $requestedStatus);
            }

            return $product->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(Product $product, array $data): Product
    {
        return DB::transaction(function () use ($product, $data): Product {
            $storeId = $product->store_id;
            $requestedStatus = $this->requestedProductStatus($data);

            $product->fill(Arr::only($data, [
                'title',
                'description_html',
                'vendor',
                'product_type',
                'tags',
            ]));

            if (array_key_exists('handle', $data)) {
                $product->handle = $data['handle'];
            } elseif (array_key_exists('title', $data)) {
                $product->handle = $this->handles->generate($data['title'], 'products', $storeId, $product->getKey());
            }

            $product->save();
            $product = $product->refresh();

            if ($requestedStatus !== null && $requestedStatus !== $this->productStatus($product)) {
                $this->transitionStatus($product, $requestedStatus);
            }

            return $product->refresh();
        });
    }

    public function transitionStatus(Product $product, ProductStatus $newStatus): void
    {
        $currentStatus = $this->productStatus($product);

        if ($currentStatus === $newStatus) {
            return;
        }

        if ($newStatus === ProductStatus::Active) {
            $this->assertCanActivate($product);
        }

        if ($newStatus === ProductStatus::Draft && $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
        }

        if (! $this->isAllowedTransition($currentStatus, $newStatus)) {
            throw InvalidProductTransitionException::because("Cannot transition product from {$currentStatus->value} to {$newStatus->value}.");
        }

        $product->forceFill([
            'status' => $newStatus,
            'published_at' => $newStatus === ProductStatus::Active
                ? ($product->published_at ?? now())
                : $product->published_at,
        ])->save();

        ProductStatusChanged::dispatch($product->refresh(), $currentStatus, $newStatus);
    }

    public function delete(Product $product): void
    {
        if ($this->productStatus($product) !== ProductStatus::Draft || $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Only draft products with no order history can be deleted.');
        }

        $product->delete();
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function createVariant(Product $product, Store $store, array $variantData): ProductVariant
    {
        $this->assertSkuIsUnique($store, $variantData['sku'] ?? null);

        $variant = $product->variants()->create([
            ...Arr::only($variantData, [
                'sku',
                'barcode',
                'price_amount',
                'compare_at_amount',
                'currency',
                'weight_g',
                'requires_shipping',
                'is_default',
                'position',
                'status',
            ]),
            'currency' => $variantData['currency'] ?? $store->default_currency,
            'status' => $variantData['status'] ?? VariantStatus::Active,
        ]);

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

        $this->syncVariantOptionValues($product, $variant, $variantData);

        return $variant;
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function syncVariantOptionValues(Product $product, ProductVariant $variant, array $variantData): void
    {
        if (isset($variantData['option_value_ids'])) {
            $valueIds = collect($variantData['option_value_ids'])
                ->map(fn (mixed $valueId): int => (int) $valueId)
                ->values()
                ->all();

            $validCount = ProductOptionValue::query()
                ->whereIn('id', $valueIds)
                ->whereHas('option', fn ($query) => $query->where('product_id', $product->getKey()))
                ->count();

            if ($validCount !== count($valueIds)) {
                throw new InvalidArgumentException('Variant option values must belong to the product being created.');
            }

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

            return;
        }

        if (! isset($variantData['options']) || ! is_array($variantData['options'])) {
            return;
        }

        $valueIds = [];

        foreach ($variantData['options'] as $optionName => $optionValue) {
            $valueId = ProductOptionValue::query()
                ->where('value', (string) $optionValue)
                ->whereHas('option', function ($query) use ($product, $optionName): void {
                    $query
                        ->where('product_id', $product->getKey())
                        ->where('name', (string) $optionName);
                })
                ->value('id');

            if ($valueId === null) {
                throw new InvalidArgumentException("Variant option selection [{$optionName}: {$optionValue}] is invalid for this product.");
            }

            $valueIds[] = (int) $valueId;
        }

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

    private function assertCanActivate(Product $product): void
    {
        if (trim((string) $product->title) === '') {
            throw InvalidProductTransitionException::because('A product title is required before activation.');
        }

        if (! $product->variants()->where('price_amount', '>', 0)->exists()) {
            throw InvalidProductTransitionException::because('At least one priced variant is required before activation.');
        }
    }

    private function isAllowedTransition(ProductStatus $from, ProductStatus $to): bool
    {
        return match ($from) {
            ProductStatus::Draft => in_array($to, [ProductStatus::Active, ProductStatus::Archived], true),
            ProductStatus::Active => in_array($to, [ProductStatus::Archived, ProductStatus::Draft], true),
            ProductStatus::Archived => in_array($to, [ProductStatus::Active, ProductStatus::Draft], true),


sed -n '320,760p' app/Livewire/Admin/Products/Form.php

                ProductMedia::withoutGlobalScopes()
                    ->whereKey($mediaId)
                    ->update(['position' => $position]);
            }
        });

        $this->refreshProductMedia();
    }

    public function removeMedia(int $mediaId): void
    {
        $media = $this->mediaRecord($mediaId);

        $media->delete();

        $this->reorderMedia(
            collect($this->media)
                ->pluck('id')
                ->reject(fn (int $id): bool => $id === $mediaId)
                ->values()
                ->all(),
        );

        session()->flash('status', 'Media removed');
    }

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,
        ])->layout('layouts.app', [
            'title' => $this->product ? $this->product->title : __('Add product'),
        ]);
    }

    private function fillFromProduct(Product $product): void
    {
        $this->title = $product->title;
        $this->descriptionHtml = (string) $product->description_html;
        $this->status = $product->status->value;
        $this->vendor = (string) $product->vendor;
        $this->productType = (string) $product->product_type;
        $this->tags = implode(', ', $product->tags ?? []);
        $this->handle = $product->handle;
        $this->publishedAt = $product->published_at?->format('Y-m-d\TH:i');
        $this->collectionIds = $product->collections->pluck('id')->map(fn (int $id): int => $id)->all();
        $this->media = $product->media
            ->map(fn (ProductMedia $media): array => [
                'id' => $media->getKey(),
                'url' => Storage::disk('public')->url($media->storage_key),
                'exists' => Storage::disk('public')->exists($media->storage_key),
                'altText' => (string) $media->alt_text,
                'position' => $media->position,
                'status' => $media->status->value,
            ])
            ->values()
            ->all();
        $this->options = $product->options
            ->map(fn (ProductOption $option): array => [
                'name' => $option->name,
                'values' => $option->values->pluck('value')->implode(', '),
            ])
            ->all();
        $this->variants = $product->variants
            ->map(fn (ProductVariant $variant): array => [
                'id' => $variant->getKey(),
                'label' => $variant->optionValues->isEmpty()
                    ? 'Default'
                    : $variant->optionValues->map(fn (ProductOptionValue $value): string => $value->value)->implode(' / '),
                'sku' => (string) $variant->sku,
                'price' => number_format($variant->price_amount / 100, 2, '.', ''),
                'compareAtPrice' => $variant->compare_at_amount ? number_format($variant->compare_at_amount / 100, 2, '.', '') : '',
                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                'requiresShipping' => $variant->requires_shipping,
                'options' => $variant->optionValues
                    ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option?->name ?? 'Option' => $value->value])
                    ->all(),
            ])
            ->values()
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    private function productPayload(): array
    {
        return [
            'title' => $this->title,
            'description_html' => $this->descriptionHtml ?: null,
            'status' => $this->status,
            'vendor' => $this->vendor ?: null,
            'product_type' => $this->productType ?: null,
            'tags' => collect(explode(',', $this->tags))
                ->map(fn (string $tag): string => trim($tag))
                ->filter()
                ->values()
                ->all(),
            'handle' => Str::slug($this->handle),
        ];
    }

    /**
     * @return array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>
     */
    private function optionPayload(): array
    {
        return collect($this->options)
            ->map(function (array $option, int $position): array {
                return [
                    'name' => $option['name'],
                    'position' => $position,
                    'values' => collect(explode(',', $option['values']))
                        ->map(fn (string $value): string => trim($value))
                        ->filter(…6422 chars truncated…}

        return collect($options)
            ->reduce(function (array $combinations, array $option): array {
                $next = [];

                foreach ($combinations as $combination) {
                    foreach ($option['values'] as $value) {
                        $next[] = [
                            ...$combination,
                            $option['name'] => $value['value'],
                        ];
                    }
                }

                return $next;
            }, [[]]);
    }

    /**
     * @param  array<string, string>  $options
     */
    private function variantOptionKey(array $options): string
    {
        ksort($options);

        return collect($options)
            ->map(fn (string $value, string $name): string => "{$name}:{$value}")
            ->implode('|');
    }

    /**
     * @param  array<string, string>  $options
     */
    private function variantLabel(array $options): string
    {
        return $options === [] ? 'Default' : implode(' / ', array_values($options));
    }

    /**
     * @param  array<string, string>  $options
     * @return list<int>
     */
    private function optionValueIdsForVariant(Product $product, array $options): array
    {
        if ($options === []) {
            return [];
        }

        return collect($options)
            ->map(function (string $value, string $optionName) use ($product): int {
                return (int) ProductOptionValue::withoutGlobalScopes()
                    ->where('value', $value)
                    ->whereHas('option', function ($query) use ($optionName, $product): void {
                        $query
                            ->withoutGlobalScopes()
                            ->where('product_id', $product->getKey())
                            ->where('name', $optionName);
                    })
                    ->value('id');
            })
            ->filter()
            ->values()
            ->all();
    }

    private function variantHasOrderLines(ProductVariant $variant): bool
    {
        return Schema::hasTable('order_lines')
            && DB::table('order_lines')->where('variant_id', $variant->getKey())->exists();
    }

    private function validateActiveVariantPricing(): bool
    {
        if ($this->status !== ProductStatus::Active->value) {
            return true;
        }

        $hasPricedVariant = collect($this->variants)
            ->contains(fn (array $variant): bool => Money::fromDecimalString($variant['price']) > 0);

        if ($hasPricedVariant) {
            return true;
        }

        $this->addError('variants.0.price', __('At least one priced variant is required before activation.'));

        return false;
    }

    private function validateVariantSkus(Store $store): bool
    {
        $skus = collect($this->variants)
            ->pluck('sku')
            ->map(fn (?string $sku): string => trim((string) $sku))
            ->filter()
            ->values();

        if ($skus->isEmpty()) {
            return true;
        }

        if ($skus->duplicates()->isNotEmpty()) {
            $this->addError('variants.0.sku', __('Each variant SKU must be unique for this store.'));

            return false;
        }

        $variantIds = collect($this->variants)
            ->pluck('id')
            ->filter()
            ->map(fn (mixed $variantId): int => (int) $variantId)
            ->values()
            ->all();

        $conflictingSku = ProductVariant::withoutGlobalScopes()
            ->whereIn('sku', $skus->all())
            ->whereHas('product', function ($query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            })
            ->when($variantIds !== [], fn ($query) => $query->whereKeyNot($variantIds))
            ->value('sku');

        if ($conflictingSku === null) {
            return true;
        }

        $this->addError('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));

        return false;
    }

    private function storeNewMedia(Product $product): void
    {
        $maxPosition = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->max('position');
        $position = $maxPosition === null ? 0 : ((int) $maxPosition) + 1;

        foreach ($this->newMedia as $file) {
            $extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'jpg');
            $storageKey = $file->storeAs(
                "media/originals/{$product->getKey()}",
                Str::uuid().'.'.$extension,
                'public',
            );

            $media = ProductMedia::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'type' => MediaType::Image,
                'storage_key' => $storageKey,
                'alt_text' => $product->title,


Output:
Chunk ID: b20e45
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4106
Output:
                ProductMedia::withoutGlobalScopes()
                    ->whereKey($mediaId)
                    ->update(['position' => $position]);
            }
        });

        $this->refreshProductMedia();
    }

    public function removeMedia(int $mediaId): void
    {
        $media = $this->mediaRecord($mediaId);

        $media->delete();

        $this->reorderMedia(
            collect($this->media)
                ->pluck('id')
                ->reject(fn (int $id): bool => $id === $mediaId)
                ->values()
                ->all(),
        );

        session()->flash('status', 'Media removed');
    }

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,
        ])->layout('layouts.app', [
            'title' => $this->product ? $this->product->title : __('Add product'),
        ]);
    }

    private function fillFromProduct(Product $product): void
    {
        $this->title = $product->title;
        $this->descriptionHtml = (string) $product->description_html;
        $this->status = $product->status->value;
        $this->vendor = (string) $product->vendor;
        $this->productType = (string) $product->product_type;
        $this->tags = implode(', ', $product->tags ?? []);
        $this->handle = $product->handle;
        $this->publishedAt = $product->published_at?->format('Y-m-d\TH:i');
        $this->collectionIds = $product->collections->pluck('id')->map(fn (int $id): int => $id)->all();
        $this->media = $product->media
            ->map(fn (ProductMedia $media): array => [
                'id' => $media->getKey(),
                'url' => Storage::disk('public')->url($media->storage_key),
                'exists' => Storage::disk('public')->exists($media->storage_key),
                'altText' => (string) $media->alt_text,
                'position' => $media->position,
                'status' => $media->status->value,
            ])
            ->values()
            ->all();
        $this->options = $product->options
            ->map(fn (ProductOption $option): array => [
                'name' => $option->name,
                'values' => $option->values->pluck('value')->implode(', '),
            ])
            ->all();
        $this->variants = $product->variants
            ->map(fn (ProductVariant $variant): array => [
                'id' => $variant->getKey(),
                'label' => $variant->optionValues->isEmpty()
                    ? 'Default'
                    : $variant->optionValues->map(fn (ProductOptionValue $value): string => $value->value)->implode(' / '),
                'sku' => (string) $variant->sku,
                'price' => number_format($variant->price_amount / 100, 2, '.', ''),
                'compareAtPrice' => $variant->compare_at_amount ? number_format($variant->compare_at_amount / 100, 2, '.', '') : '',
                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                'requiresShipping' => $variant->requires_shipping,
                'options' => $variant->optionValues
                    ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option?->name ?? 'Option' => $value->value])
                    ->all(),
            ])
            ->values()
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    private function productPayload(): array
    {
        return [
            'title' => $this->title,
            'description_html' => $this->descriptionHtml ?: null,
            'status' => $this->status,
            'vendor' => $this->vendor ?: null,
            'product_type' => $this->productType ?: null,
            'tags' => collect(explode(',', $this->tags))
                ->map(fn (string $tag): string => trim($tag))
                ->filter()
                ->values()
                ->all(),
            'handle' => Str::slug($this->handle),
        ];
    }

    /**
     * @return array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>
     */
    private function optionPayload(): array
    {
        return collect($this->options)
            ->map(function (array $option, int $position): array {
                return [
                    'name' => $option['name'],
                    'position' => $position,
                    'values' => collect(explode(',', $option['values']))
                        ->map(fn (string $value): string => trim($value))
                        ->filter()
                        ->values()
                        ->map(fn (string $value, int $valuePosition): array => [
                            'value' => $value,
                            'position' => $valuePosition,
                        ])
                        ->all(),
                ];
            })
            ->filter(fn (array $option): bool => $option['name'] !== '' && $option['values'] !== [])
            ->values()
            ->all();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function variantPayload(Store $store): array
    {
        return collect($this->variants)
            ->map(fn (array $variant, int $position): array => [
                'sku' => $variant['sku'] ?: null,
                'price_amount' => Money::fromDecimalString($variant['price']),
                'compare_at_amount' => $variant['compareAtPrice'] === '' ? null : Money::fromDecimalString($variant['compareAtPrice']),
                'currency' => $store->default_currency,
                'quantity_on_hand' => (int) $variant['quantity'],
                'requires_shipping' => (bool) $variant['requiresShipping'],
                'is_default' => $position === 0,
                'position' => $position,
                'options' => $variant['options'] ?? [],
            ])
            ->all();
    }

    private function syncExistingVariants(Product $product, Store $store): void
    {
        $syncedVariantIds = [];

        foreach ($this->variantPayload($store) as $position => $variantData) {
            $variantId = $this->variants[$position]['id'] ?? null;
            $variant = $variantId
                ? ProductVariant::withoutGlobalScopes()->where('product_id', $product->getKey())->findOrFail($variantId)
                : $product->variants()->create(['position' => $position]);

            $variant->forceFill([
                'sku' => $variantData['sku'],
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'],
                'currency' => $variantData['currency'],
                'requires_shipping' => $variantData['requires_shipping'],
                'is_default' => $position === 0,
                'position' => $position,
            ])->save();

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

            $variant->optionValues()->sync($this->optionValueIdsForVariant($product, $variantData['options'] ?? []));
            $syncedVariantIds[] = $variant->getKey();
        }

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedVariantIds !== [], fn ($query) => $query->whereNotIn('id', $syncedVariantIds))
            ->get()
            ->each(function (ProductVariant $variant): void {
                if ($this->variantHasOrderLines($variant)) {
                    $variant->forceFill(['status' => VariantStatus::Archived])->save();

                    return;
                }

                $variant->delete();
            });
    }

    private function syncProductOptions(Product $product): void
    {
        $optionPayload = $this->optionPayload();
        $syncedOptionIds = [];

        foreach ($optionPayload as $optionData) {
            $option = ProductOption::withoutGlobalScopes()
                ->where('product_id', $product->getKey())
                ->where('position', $optionData['position'])
                ->first()
                ?? $product->options()->create([
                    'name' => $optionData['name'],
                    'position' => $optionData['position'],
                ]);

            $option->forceFill([
                'name' => $optionData['name'],
                'position' => $optionData['position'],
            ])->save();

            $syncedOptionIds[] = $option->getKey();
            $syncedValueIds = [];

            foreach ($optionData['values'] as $valueData) {
                $value = ProductOptionValue::withoutGlobalScopes()
                    ->where('product_option_id', $option->getKey())
                    ->where('position', $valueData['position'])
                    ->first()
                    ?? $option->values()->create([
                        'value' => $valueData['value'],
                        'position' => $valueData['position'],
                    ]);

                $value->forceFill([
                    'value' => $valueData['value'],
                    'position' => $valueData['position'],
                ])->save();

                $syncedValueIds[] = $value->getKey();
            }

            ProductOptionValue::withoutGlobalScopes()
                ->where('product_option_id', $option->getKey())
                ->when($syncedValueIds !== [], fn ($query) => $query->whereNotIn('id', $syncedValueIds))
                ->delete();
        }

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedOptionIds !== [], fn ($query) => $query->whereNotIn('id', $syncedOptionIds))
            ->delete();
    }

    private function ensureVariantMatrixMatchesOptions(): void
    {
        $desiredKeys = collect($this->optionCombinations($this->optionPayload()))
            ->map(fn (array $options): string => $this->variantOptionKey($options))
            ->sort()
            ->values()
            ->all();
        $currentKeys = collect($this->variants)
            ->map(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []))
            ->sort()
            ->values()
            ->all();

        if ($desiredKeys !== $currentKeys) {
            $this->generateVariants();
        }
    }

    /**
     * @param  array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>  $options
     * @return array<int, array<string, string>>
     */
    private function optionCombinations(array $options): array
    {
        if ($options === []) {
            return [[]];
        }

        return collect($options)
            ->reduce(function (array $combinations, array $option): array {
                $next = [];

                foreach ($combinations as $combination) {
                    foreach ($option['values'] as $value) {
                        $next[] = [
                            ...$combination,
                            $option['name'] => $value['value'],
                        ];
                    }
                }

                return $next;
            }, [[]]);
    }

    /**
     * @param  array<string, string>  $options
     */
    private function variantOptionKey(array $options): string
    {
        ksort($options);

        return collect($options)
            ->map(fn (string $value, string $name): string => "{$name}:{$value}")
            ->implode('|');
    }

    /**
     * @param  array<string, string>  $options
     */
    private function variantLabel(array $options): string
    {
        return $options === [] ? 'Default' : implode(' / ', array_values($options));
    }

    /**
     * @param  array<string, string>  $options
     * @return list<int>
     */
    private function optionValueIdsForVariant(Product $product, array $options): array
    {
        if ($options === []) {
            return [];
        }

        return collect($options)
            ->map(function (string $value, string $optionName) use ($product): int {
                return (int) ProductOptionValue::withoutGlobalScopes()
                    ->where('value', $value)
                    ->whereHas('option', function ($query) use ($optionName, $product): void {
                        $query
                            ->withoutGlobalScopes()
                            ->where('product_id', $product->getKey())
                            ->where('name', $optionName);
                    })
                    ->value('id');
            })
            ->filter()
            ->values()
            ->all();
    }

    private function variantHasOrderLines(ProductVariant $variant): bool
    {
        return Schema::hasTable('order_lines')
            && DB::table('order_lines')->where('variant_id', $variant->getKey())->exists();
    }

    private function validateActiveVariantPricing(): bool
    {
        if ($this->status !== ProductStatus::Active->value) {
            return true;
        }

        $hasPricedVariant = collect($this->variants)
            ->contains(fn (array $variant): bool => Money::fromDecimalString($variant['price']) > 0);

        if ($hasPricedVariant) {
            return true;
        }

        $this->addError('variants.0.price', __('At least one priced variant is required before activation.'));

        return false;
    }

    private function validateVariantSkus(Store $store): bool
    {
        $skus = collect($this->variants)
            ->pluck('sku')
            ->map(fn (?string $sku): string => trim((string) $sku))
            ->filter()
            ->values();

        if ($skus->isEmpty()) {
            return true;
        }

        if ($skus->duplicates()->isNotEmpty()) {
            $this->addError('variants.0.sku', __('Each variant SKU must be unique for this store.'));

            return false;
        }

        $variantIds = collect($this->variants)
            ->pluck('id')
            ->filter()
            ->map(fn (mixed $variantId): int => (int) $variantId)
            ->values()
            ->all();

        $conflictingSku = ProductVariant::withoutGlobalScopes()
            ->whereIn('sku', $skus->all())
            ->whereHas('product', function ($query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            })
            ->when($variantIds !== [], fn ($query) => $query->whereKeyNot($variantIds))
            ->value('sku');

        if ($conflictingSku === null) {
            return true;
        }

        $this->addError('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));

        return false;
    }

    private function storeNewMedia(Product $product): void
    {
        $maxPosition = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->max('position');
        $position = $maxPosition === null ? 0 : ((int) $maxPosition) + 1;

        foreach ($this->newMedia as $file) {
            $extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'jpg');
            $storageKey = $file->storeAs(
                "media/originals/{$product->getKey()}",
                Str::uuid().'.'.$extension,
                'public',
            );

            $media = ProductMedia::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'type' => MediaType::Image,
                'storage_key' => $storageKey,
                'alt_text' => $product->title,


Output:
Chunk ID: 10c3a0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2321
Output:
<?php

namespace App\Services;

use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Events\ProductStatusChanged;
use App\Exceptions\InvalidProductTransitionException;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Support\HandleGenerator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use InvalidArgumentException;
use RuntimeException;

class ProductService
{
    public function __construct(
        private readonly HandleGenerator $handles,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(Store $store, array $data): Product
    {
        return DB::transaction(function () use ($store, $data): Product {
            $requestedStatus = $this->requestedProductStatus($data) ?? ProductStatus::Draft;

            $product = Product::query()->create([
                ...Arr::only($data, [
                    'title',
                    'description_html',
                    'vendor',
                    'product_type',
                    'tags',
                ]),
                'store_id' => $store->getKey(),
                'handle' => $data['handle'] ?? $this->handles->generate($data['title'], 'products', $store->getKey()),
                'status' => ProductStatus::Draft,
                'published_at' => null,
            ]);

            foreach ($data['options'] ?? [] as $optionData) {
                $option = $product->options()->create(Arr::only($optionData, ['name', 'position']));

                foreach ($optionData['values'] ?? [] as $valueData) {
                    $option->values()->create(Arr::only($valueData, ['value', 'position']));
                }
            }

            $variants = $data['variants'] ?? [];

            if ($variants === []) {
                $variants = [[
                    'sku' => $data['sku'] ?? null,
                    'price_amount' => $data['price_amount'] ?? 0,
                    'currency' => $store->default_currency,
                    'is_default' => true,
                    'position' => 0,
                ]];
            }

            foreach ($variants as $variantData) {
                $this->createVariant($product, $store, $variantData);
            }

            if ($requestedStatus !== ProductStatus::Draft) {
                $this->transitionStatus($product->refresh(), $requestedStatus);
            }

            return $product->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(Product $product, array $data): Product
    {
        return DB::transaction(function () use ($product, $data): Product {
            $storeId = $product->store_id;
            $requestedStatus = $this->requestedProductStatus($data);

            $product->fill(Arr::only($data, [
                'title',
                'description_html',
                'vendor',
                'product_type',
                'tags',
            ]));

            if (array_key_exists('handle', $data)) {
                $product->handle = $data['handle'];
            } elseif (array_key_exists('title', $data)) {
                $product->handle = $this->handles->generate($data['title'], 'products', $storeId, $product->getKey());
            }

            $product->save();
            $product = $product->refresh();

            if ($requestedStatus !== null && $requestedStatus !== $this->productStatus($product)) {
                $this->transitionStatus($product, $requestedStatus);
            }

            return $product->refresh();
        });
    }

    public function transitionStatus(Product $product, ProductStatus $newStatus): void
    {
        $currentStatus = $this->productStatus($product);

        if ($currentStatus === $newStatus) {
            return;
        }

        if ($newStatus === ProductStatus::Active) {
            $this->assertCanActivate($product);
        }

        if ($newStatus === ProductStatus::Draft && $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
        }

        if (! $this->isAllowedTransition($currentStatus, $newStatus)) {
            throw InvalidProductTransitionException::because("Cannot transition product from {$currentStatus->value} to {$newStatus->value}.");
        }

        $product->forceFill([
            'status' => $newStatus,
            'published_at' => $newStatus === ProductStatus::Active
                ? ($product->published_at ?? now())
                : $product->published_at,
        ])->save();

        ProductStatusChanged::dispatch($product->refresh(), $currentStatus, $newStatus);
    }

    public function delete(Product $product): void
    {
        if ($this->productStatus($product) !== ProductStatus::Draft || $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Only draft products with no order history can be deleted.');
        }

        $product->delete();
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function createVariant(Product $product, Store $store, array $variantData): ProductVariant
    {
        $this->assertSkuIsUnique($store, $variantData['sku'] ?? null);

        $variant = $product->variants()->create([
            ...Arr::only($variantData, [
                'sku',
                'barcode',
                'price_amount',
                'compare_at_amount',
                'currency',
                'weight_g',
                'requires_shipping',
                'is_default',
                'position',
                'status',
            ]),
            'currency' => $variantData['currency'] ?? $store->default_currency,
            'status' => $variantData['status'] ?? VariantStatus::Active,
        ]);

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

        $this->syncVariantOptionValues($product, $variant, $variantData);

        return $variant;
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function syncVariantOptionValues(Product $product, ProductVariant $variant, array $variantData): void
    {
        if (isset($variantData['option_value_ids'])) {
            $valueIds = collect($variantData['option_value_ids'])
                ->map(fn (mixed $valueId): int => (int) $valueId)
                ->values()
                ->all();

            $validCount = ProductOptionValue::query()
                ->whereIn('id', $valueIds)
                ->whereHas('option', fn ($query) => $query->where('product_id', $product->getKey()))
                ->count();

            if ($validCount !== count($valueIds)) {
                throw new InvalidArgumentException('Variant option values must belong to the product being created.');
            }

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

            return;
        }

        if (! isset($variantData['options']) || ! is_array($variantData['options'])) {
            return;
        }

        $valueIds = [];

        foreach ($variantData['options'] as $optionName => $optionValue) {
            $valueId = ProductOptionValue::query()
                ->where('value', (string) $optionValue)
                ->whereHas('option', function ($query) use ($product, $optionName): void {
                    $query
                        ->where('product_id', $product->getKey())
                        ->where('name', (string) $optionName);
                })
                ->value('id');

            if ($valueId === null) {
                throw new InvalidArgumentException("Variant option selection [{$optionName}: {$optionValue}] is invalid for this product.");
            }

            $valueIds[] = (int) $valueId;
        }

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

    private function assertCanActivate(Product $product): void
    {
        if (trim((string) $product->title) === '') {
            throw InvalidProductTransitionException::because('A product title is required before activation.');
        }

        if (! $product->variants()->where('price_amount', '>', 0)->exists()) {
            throw InvalidProductTransitionException::because('At least one priced variant is required before activation.');
        }
    }

    private function isAllowedTransition(ProductStatus $from, ProductStatus $to): bool
    {
        return match ($from) {
            ProductStatus::Draft => in_array($to, [ProductStatus::Active, ProductStatus::Archived], true),
            ProductStatus::Active => in_array($to, [ProductStatus::Archived, ProductStatus::Draft], true),
            ProductStatus::Archived => in_array($to, [ProductStatus::Active, ProductStatus::Draft], true),