sed -n '1,220p' database/factories/ProductFactory.php && sed -n '1,220p' database/seeders/ProductSeeder.php

ProductSeeder.php

<?php

namespace Database\Factories;

use App\Enums\ProductStatus;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(3, true));

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'status' => ProductStatus::Active,
            'description_html' => '<p>'.fake()->paragraph().'</p>',
            'vendor' => fake()->company(),
            'product_type' => fake()->randomElement(['Shirts', 'Pants', 'Shoes', 'Accessories', 'Electronics', 'Books']),
            'tags' => fake()->randomElements(['new', 'sale', 'trending', 'popular', 'limited'], fake()->numberBetween(1, 3)),
            'published_at' => now(),
        ];
    }

    public function draft(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Draft,
            'published_at' => null,
        ]);
    }

    public function archived(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Archived,
        ]);
    }

    public function withVariants(int $count): static
    {
        return $this->afterCreating(function ($product) use ($count): void {
            ProductVariant::factory()
                ->count($count)
                ->for($product)
                ->create();
        });
    }

    public function withDefaultVariant(int $price = 2499): static
    {
        return $this->afterCreating(function ($product) use ($price): void {
            $variant = ProductVariant::factory()
                ->default()
                ->for($product)
                ->create([
                    'price_amount' => $price,
                    'currency' => $product->store->default_currency,
                ]);

            InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->update([
                'quantity_on_hand' => 50,
            ]);
        });
    }
}
<?php

namespace Database\Seeders;

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

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        DB::transaction(function (): void {
            $this->seedProducts('acme-fashion', $this->fashionProducts());
            $this->seedProducts('acme-electronics', $this->electronicsProducts());
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $products
     */
    private function seedProducts(string $storeHandle, array $products): void
    {
        $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
        $collections = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->keyBy('handle');

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

            $this->replaceCatalogChildren($product);

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

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

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

    private function replaceCatalogChildren(Product $product): void
    {
        ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
           …131 chars truncated…
            ->delete();
    }

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

        foreach ($options as $optionPosition => $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $option = ProductOption::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'name' => $optionName,
                'position' => $optionPosition,
            ]);

            foreach (array_values($valuesByName[$optionName]) as $valuePosition => $value) {
                $optionValue = ProductOptionValue::withoutGlobalScopes()->create([
                    'product_option_id' => $option->getKey(),
                    'value' => $value,
                    'position' => $valuePosition,
                ]);

                $optionValueIds[$optionName][$value] = $optionValue->getKey();
            }
        }

        return $optionValueIds;
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, array<string, int>>  $optionValueIds
     */
    private function createVariants(Store $store, Product $product, array $data, array $optionValueIds): void
    {
        $variants = $data['variants'] ?? $this->variantDefinitions($data);

        foreach ($variants as $position => $variantData) {
            $variant = ProductVariant::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'sku' => $variantData['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), $variantData['options'] ?? []),
                'barcode' => $variantData['barcode'] ?? null,
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'] ?? null,
                'currency' => $store->default_currency,
                'weight_g' => $variantData['weight_g'] ?? 250,
                'requires_shipping' => $variantData['requires_shipping'] ?? true,
                'is_default' => $position === 0,
                'position' => $position,
                'status' => $variantData['status'] ?? '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['policy'] ?? 'deny',
                ],
            );

            $selectedOptions = $variantData['options'] ?? [];

            if ($selectedOptions !== []) {
                $variant->optionValues()->sync($this->selectedOptionValueIds($selectedOptions, $optionValueIds));
            }
        }
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, array<string, mixed>>
     */
    private function variantDefinitions(array $data): array
    {
        $options = $data['options'] ?? [];
        $defaults = $data['variant_defaults'];

        if ($options === []) {
            return [[
                ...$defaults,
                'options' => [],
                'sku' => $data['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), []),
            ]];
        }

        return collect($this->optionCombinations($options))
            ->map(fn (array $selection): array => [
                ...$defaults,
                'options' => $selection,
            ])
            ->all();
    }

    /**
     * @param  array<int, array<string, array<int, string>>>  $options
     * @return array<int, array<string, string>>
     */
    private function optionCombinations(array $options): array
    {
        $combinations = [[]];

        foreach ($options as $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $next = [];

            foreach ($combinations as $combination) {
                foreach ($valuesByName[$optionName] as $value) {
                    $next[] = [
                        ...$combination,
                        $optionName => $value,
                    ];
                }
            }

            $combinations = $next;
        }

        return $combinations;
    }

    /**
     * @param  array<string, string>  $selectedOptions
     * @param  array<string, array<string, int>>  $optionValueIds
     * @return array<int, int>
     */
    private function selectedOptionValueIds(array $selectedOptions, array $optionValueIds): array
    {
        return collect($selectedOptions)
            ->map(fn (string $value, string $optionName): int => $optionValueIds[$optionName][$value])
            ->values()
            ->all();
    }


sed -n '1,220p' database/migrations/2026_05_03_213856_create_products_table.php && sed -n '1,180p' database/migrations/2026_05_03_223841_create_pages_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->enum('status', ['draft', 'active', 'archived'])->default('draft');
            $table->text('description_html')->nullable();
            $table->string('vendor')->nullable();
            $table->string('product_type')->nullable();
            $table->text('tags')->default('[]');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'published_at']);
            $table->index(['store_id', 'vendor']);
            $table->index(['store_id', 'product_type']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('body_html')->nullable();
            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('pages');
    }
};


Output:
Chunk ID: 26581a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 594
Output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->enum('status', ['draft', 'active', 'archived'])->default('draft');
            $table->text('description_html')->nullable();
            $table->string('vendor')->nullable();
            $table->string('product_type')->nullable();
            $table->text('tags')->default('[]');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'published_at']);
            $table->index(['store_id', 'vendor']);
            $table->index(['store_id', 'product_type']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('handle');
            $table->text('body_html')->nullable();
            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'handle']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('pages');
    }
};


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

namespace Database\Factories;

use App\Enums\ProductStatus;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(3, true));

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'status' => ProductStatus::Active,
            'description_html' => '<p>'.fake()->paragraph().'</p>',
            'vendor' => fake()->company(),
            'product_type' => fake()->randomElement(['Shirts', 'Pants', 'Shoes', 'Accessories', 'Electronics', 'Books']),
            'tags' => fake()->randomElements(['new', 'sale', 'trending', 'popular', 'limited'], fake()->numberBetween(1, 3)),
            'published_at' => now(),
        ];
    }

    public function draft(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Draft,
            'published_at' => null,
        ]);
    }

    public function archived(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Archived,
        ]);
    }

    public function withVariants(int $count): static
    {
        return $this->afterCreating(function ($product) use ($count): void {
            ProductVariant::factory()
                ->count($count)
                ->for($product)
                ->create();
        });
    }

    public function withDefaultVariant(int $price = 2499): static
    {
        return $this->afterCreating(function ($product) use ($price): void {
            $variant = ProductVariant::factory()
                ->default()
                ->for($product)
                ->create([
                    'price_amount' => $price,
                    'currency' => $product->store->default_currency,
                ]);

            InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->update([
                'quantity_on_hand' => 50,
            ]);
        });
    }
}
<?php

namespace Database\Seeders;

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

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        DB::transaction(function (): void {
            $this->seedProducts('acme-fashion', $this->fashionProducts());
            $this->seedProducts('acme-electronics', $this->electronicsProducts());
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $products
     */
    private function seedProducts(string $storeHandle, array $products): void
    {
        $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
        $collections = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->keyBy('handle');

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

            $this->replaceCatalogChildren($product);

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

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

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

    private function replaceCatalogChildren(Product $product): void
    {
        ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->delete();
    }

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

        foreach ($options as $optionPosition => $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $option = ProductOption::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'name' => $optionName,
                'position' => $optionPosition,
            ]);

            foreach (array_values($valuesByName[$optionName]) as $valuePosition => $value) {
                $optionValue = ProductOptionValue::withoutGlobalScopes()->create([
                    'product_option_id' => $option->getKey(),
                    'value' => $value,
                    'position' => $valuePosition,
                ]);

                $optionValueIds[$optionName][$value] = $optionValue->getKey();
            }
        }

        return $optionValueIds;
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, array<string, int>>  $optionValueIds
     */
    private function createVariants(Store $store, Product $product, array $data, array $optionValueIds): void
    {
        $variants = $data['variants'] ?? $this->variantDefinitions($data);

        foreach ($variants as $position => $variantData) {
            $variant = ProductVariant::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'sku' => $variantData['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), $variantData['options'] ?? []),
                'barcode' => $variantData['barcode'] ?? null,
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'] ?? null,
                'currency' => $store->default_currency,
                'weight_g' => $variantData['weight_g'] ?? 250,
                'requires_shipping' => $variantData['requires_shipping'] ?? true,
                'is_default' => $position === 0,
                'position' => $position,
                'status' => $variantData['status'] ?? '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['policy'] ?? 'deny',
                ],
            );

            $selectedOptions = $variantData['options'] ?? [];

            if ($selectedOptions !== []) {
                $variant->optionValues()->sync($this->selectedOptionValueIds($selectedOptions, $optionValueIds));
            }
        }
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, array<string, mixed>>
     */
    private function variantDefinitions(array $data): array
    {
        $options = $data['options'] ?? [];
        $defaults = $data['variant_defaults'];

        if ($options === []) {
            return [[
                ...$defaults,
                'options' => [],
                'sku' => $data['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), []),
            ]];
        }

        return collect($this->optionCombinations($options))
            ->map(fn (array $selection): array => [
                ...$defaults,
                'options' => $selection,
            ])
            ->all();
    }

    /**
     * @param  array<int, array<string, array<int, string>>>  $options
     * @return array<int, array<string, string>>
     */
    private function optionCombinations(array $options): array
    {
        $combinations = [[]];

        foreach ($options as $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $next = [];

            foreach ($combinations as $combination) {
                foreach ($valuesByName[$optionName] as $value) {
                    $next[] = [
                        ...$combination,
                        $optionName => $value,
                    ];
                }
            }

            $combinations = $next;
        }

        return $combinations;
    }

    /**
     * @param  array<string, string>  $selectedOptions
     * @param  array<string, array<string, int>>  $optionValueIds
     * @return array<int, int>
     */
    private function selectedOptionValueIds(array $selectedOptions, array $optionValueIds): array
    {
        return collect($selectedOptions)
            ->map(fn (string $value, string $optionName): int => $optionValueIds[$optionName][$value])
            ->values()
            ->all();
    }


sed -n '1,220p' app/Http/Controllers/Api/Storefront/V1/CartController.php && sed -n '1,180p' app/Http/Resources/Storefront/V1/CartResource.php

CartResource.php

<?php

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

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartController extends Controller
{
    public function store(StoreCartRequest $request, CartService $carts): JsonResponse
    {
        $request->validated();

        return CartResource::make($this->loadCart($carts->create($this->currentStore())))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Cart $cart): CartResource
    {
        return CartResource::make($this->loadCart($cart));
    }

    private function currentStore(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}
<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CartResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        $lines = $this->whenLoaded('lines');

        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'customer_id' => $this->customer_id,
            'currency' => $this->currency,
            'status' => $this->status?->value,
            'cart_version' => $this->cart_version,
            'line_count' => $this->relationLoaded('lines') ? $this->lines->sum('quantity') : null,
            'totals' => $this->relationLoaded('lines') ? [
                'subtotal' => $this->lines->sum('line_subtotal_amount'),
                'discount' => $this->lines->sum('line_discount_amount'),
                'total' => $this->lines->sum('line_total_amount'),
                'currency' => $this->currency,
            ] : null,
            'lines' => CartLineResource::collection($lines),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


sed -n '1,220p' tests/Feature/Catalog/CatalogUiTest.php && sed -n '1,180p' tests/Feature/Api/StorefrontCartApiTest.php

StorefrontCartApiTest.php

<?php

use App\Livewire\Admin\Collections\Form as CollectionForm;
use App\Livewire\Admin\Products\Form as ProductForm;
use App\Livewire\Admin\Products\Index as ProductIndex;
use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use App\Services\ProductService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

test('storefront catalog browsing routes render seeded products', function () {
    $this->withHeader('Host', 'shop.test')
        ->get('/')
        ->assertSuccessful()
        ->assertSee('Acme Fashion')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99 EUR');

    $this->withHeader('Host', 'shop.test')
        ->get('/collections/t-shirts')
        ->assertSuccessful()
        ->assertSee('T-Shirts')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Unreleased Winter Jacket');

    $this->withHeader('Host', 'shop.test')
        ->get('/products/classic-cotton-t-shirt')
        ->assertSuccessful()
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Size')
        ->assertSee('Color')
        ->assertSee('Add to cart');

    $this->withHeader('Host', 'shop.test')
        ->get('/search?q=draft')
        ->assertSuccessful()
        ->assertDontSee('Unreleased Winter Jacket');
});

test('admin catalog routes require authentication and render for store admins', function () {
    $this->get('/admin/products')->assertRedirect('/admin/login');

    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $this->actingAs($user)
        ->get('/admin/products')
        ->assertSuccessful()
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans');

    $this->actingAs($user)
        ->get('/admin/collections')
        ->assertSuccessful()
        ->assertSee('Collections')
        ->assertSee('T-Shirts');

    $this->actingAs($user)
        ->get('/admin/inventory')
        ->assertSuccessful()
        ->assertSee('Inventory')
        ->assertSee('Limited Edition Sneakers');
});

test('admin product form creates edits archives and index filters products', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    app()->instance('current_store', $store);

    Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Test Product Created by E2E')
        ->set('handle', 'test-product-created-by-e2e')
        ->set('descriptionHtml', 'This product was created by the E2E test suite.')
        ->set('vendor', 'Test Vendor')
        ->set('productType', 'T-Shirts')
        ->set('variants.0.price', '29.99')
        ->set('variants.0.sku', 'E2E-TEST-001')
        ->set('variants.0.quantity', 50)
        ->call('save')
        ->assertSee('Product saved');

    $product = Product::query()->where('handle', 'test-product-created-by-e2e')->firstOrFail();

    expect($product->variants()->first()?->price_amount)->toBe(2999)
        ->and($product->variants()->first()?->inventoryItem?->quantity_on_hand)->toBe(50);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('title', 'Test Product Updated')
        ->call('save')
        ->assertSee('Product saved');

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('variants.0.sku', 'ACME-CTSH-S-WHT')
        ->call('save')
        ->assertHasErrors(['variants.0.sku']);

    expect($product->refresh()->variants()->first()?->sku)->toBe('E2E-TEST-001');

    $draftProduct = app(ProductService::class)->create($store, [
        'title' => 'Draft Product With Price Later',
        'handle' => 'draft-product-with-price-later',
        'status' => 'draft',
        'variants' => [[
            'sku' => 'DRAFT-PRICE-LATER-001',
            'price_amount' => 0,
            'currency' => $store->default_currency,
            'quantity_on_hand' => 4,
            'is_default' => true,
            'position' => 0,
        ]],
    ]);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $draftProduct])
        ->set('variants.0.price', '17.50')
        ->set('status', 'active')
        ->call('save')
        ->assertSee('Product saved');

    expect($draftProduct->refresh()->status->value)->toBe('active')
        ->and($draftProduct->variants()->first()?->price_amount)->toBe(1750);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('status', 'archived')
        ->call('save')
        ->assertSee('Product saved');

    expect($product->refresh()->status->value)->toBe('archived');

    Livewire::actingAs($user)
 …1575 chars truncated…rtStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function storefrontApiCartVariant(Store $store): ProductVariant
{
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'classic-cotton-t-shirt')
        ->firstOrFail();

    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->oldest('position')
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 20,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

test('storefront cart api creates and retrieves carts for the resolved store', function (): void {
    $store = storefrontApiCartStore();

    $response = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts', ['currency' => $store->default_currency]);

    $response
        ->assertCreated()
        ->assertJsonPath('data.store_id', $store->getKey())
        ->assertJsonPath('data.currency', 'EUR')
        ->assertJsonPath('data.status', 'active')
        ->assertJsonPath('data.cart_version', 1)
        ->assertJsonPath('data.line_count', 0)
        ->assertJsonPath('data.totals.total', 0);

    $this->withHeader('Host', 'shop.test')
        ->getJson("/api/storefront/v1/carts/{$response['data']['id']}")
        ->assertOk()
        ->assertJsonPath('data.id', $response['data']['id'])
        ->assertJsonPath('data.lines', []);
});

test('storefront cart api adds updates and removes line items with version increments', function (): void {
    $store = storefrontApiCartStore();
    $variant = storefrontApiCartVariant($store);

    $cartId = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $addResponse = $this->withHeader('Host', 'shop.test')
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 2,
        ]);

    $addResponse
        ->assertCreated()
        ->assertJsonPath('data.cart_version', 2)
        ->assertJsonPath('data.line_count', 2)
        ->assertJsonPath('data.lines.0.quantity', 2)
        ->assertJsonPath('data.lines.0.line_total_amount', 4998);

    $lineId = $addResponse['data']['lines'][0]['id'];

    $this->withHeader('Host', 'shop.test')
        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'quantity' => 3,
            'cart_version' => 2,
        ])
        ->assertOk()
        ->assertJsonPath('data.cart_version', 3)
        ->assertJsonPath('data.lines.0.quantity', 3)
        ->assertJsonPath('data.totals.total', 7497);

    $this->withHeader('Host', 'shop.test')
        ->deleteJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'cart_version' => 3,
        ])
        ->assertOk()
        ->assertJsonPath('data.cart_version', 4)
        ->assertJsonPath('data.line_count', 0)
        ->assertJsonPath('data.lines', []);

    expect(Cart::withoutGlobalScopes()->findOrFail($cartId)->cart_version)->toBe(4);
});

test('storefront cart api returns the current cart on version conflicts', function (): void {
    $store = storefrontApiCartStore();
    $variant = storefrontApiCartVariant($store);

    $cartId = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $lineId = $this->withHeader('Host', 'shop.test')
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 1,
        ])
        ->assertCreated()['data']['lines'][0]['id'];

    $this->withHeader('Host', 'shop.test')
        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'quantity' => 2,
            'cart_version' => 1,
        ])
        ->assertConflict()
        ->assertJsonPath('expected_cart_version', 1)
        ->assertJsonPath('current_cart_version', 2)
        ->assertJsonPath('cart.cart_version', 2)
        ->assertJsonPath('cart.lines.0.quantity', 1);
});

test('storefront cart api validates tenant and request boundaries', function (): void {
    $store = storefrontApiCartStore();

    $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts', ['currency' => 'USD'])
        ->assertUnprocessable()
        ->assertJsonValidationErrors('currency');

    $this->withHeader('Host', 'shop.test')
        ->getJson('/api/storefront/v1/carts/999999')
        ->assertNotFound();

    $otherStoreCart = Cart::factory()->create(['store_id' => Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()]);

    $this->withHeader('Host', 'shop.test')
        ->getJson("/api/storefront/v1/carts/{$otherStoreCart->getKey()}")
        ->assertNotFound();
});


Output:
Chunk ID: 9967f1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2894
Output:
<?php

use App\Livewire\Admin\Collections\Form as CollectionForm;
use App\Livewire\Admin\Products\Form as ProductForm;
use App\Livewire\Admin\Products\Index as ProductIndex;
use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use App\Services\ProductService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

test('storefront catalog browsing routes render seeded products', function () {
    $this->withHeader('Host', 'shop.test')
        ->get('/')
        ->assertSuccessful()
        ->assertSee('Acme Fashion')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99 EUR');

    $this->withHeader('Host', 'shop.test')
        ->get('/collections/t-shirts')
        ->assertSuccessful()
        ->assertSee('T-Shirts')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Unreleased Winter Jacket');

    $this->withHeader('Host', 'shop.test')
        ->get('/products/classic-cotton-t-shirt')
        ->assertSuccessful()
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Size')
        ->assertSee('Color')
        ->assertSee('Add to cart');

    $this->withHeader('Host', 'shop.test')
        ->get('/search?q=draft')
        ->assertSuccessful()
        ->assertDontSee('Unreleased Winter Jacket');
});

test('admin catalog routes require authentication and render for store admins', function () {
    $this->get('/admin/products')->assertRedirect('/admin/login');

    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $this->actingAs($user)
        ->get('/admin/products')
        ->assertSuccessful()
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans');

    $this->actingAs($user)
        ->get('/admin/collections')
        ->assertSuccessful()
        ->assertSee('Collections')
        ->assertSee('T-Shirts');

    $this->actingAs($user)
        ->get('/admin/inventory')
        ->assertSuccessful()
        ->assertSee('Inventory')
        ->assertSee('Limited Edition Sneakers');
});

test('admin product form creates edits archives and index filters products', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    app()->instance('current_store', $store);

    Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Test Product Created by E2E')
        ->set('handle', 'test-product-created-by-e2e')
        ->set('descriptionHtml', 'This product was created by the E2E test suite.')
        ->set('vendor', 'Test Vendor')
        ->set('productType', 'T-Shirts')
        ->set('variants.0.price', '29.99')
        ->set('variants.0.sku', 'E2E-TEST-001')
        ->set('variants.0.quantity', 50)
        ->call('save')
        ->assertSee('Product saved');

    $product = Product::query()->where('handle', 'test-product-created-by-e2e')->firstOrFail();

    expect($product->variants()->first()?->price_amount)->toBe(2999)
        ->and($product->variants()->first()?->inventoryItem?->quantity_on_hand)->toBe(50);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('title', 'Test Product Updated')
        ->call('save')
        ->assertSee('Product saved');

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('variants.0.sku', 'ACME-CTSH-S-WHT')
        ->call('save')
        ->assertHasErrors(['variants.0.sku']);

    expect($product->refresh()->variants()->first()?->sku)->toBe('E2E-TEST-001');

    $draftProduct = app(ProductService::class)->create($store, [
        'title' => 'Draft Product With Price Later',
        'handle' => 'draft-product-with-price-later',
        'status' => 'draft',
        'variants' => [[
            'sku' => 'DRAFT-PRICE-LATER-001',
            'price_amount' => 0,
            'currency' => $store->default_currency,
            'quantity_on_hand' => 4,
            'is_default' => true,
            'position' => 0,
        ]],
    ]);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $draftProduct])
        ->set('variants.0.price', '17.50')
        ->set('status', 'active')
        ->call('save')
        ->assertSee('Product saved');

    expect($draftProduct->refresh()->status->value)->toBe('active')
        ->and($draftProduct->variants()->first()?->price_amount)->toBe(1750);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('status', 'archived')
        ->call('save')
        ->assertSee('Product saved');

    expect($product->refresh()->status->value)->toBe('archived');

    Livewire::actingAs($user)
        ->test(ProductIndex::class)
        ->set('statusFilter', 'draft')
        ->assertSee('Unreleased Winter Jacket')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->set('statusFilter', 'active')
        ->set('search', 'Cotton')
        ->assertSee('Classic Cotton T-Shirt');
});

test('admin collection form creates and assigns products', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    $product = Product::query()->where('handle', 'classic-cotton-t-shirt')->firstOrFail();
    app()->instance('current_store', $store);

    Livewire::actingAs($user)
        ->test(CollectionForm::class)
        ->set('title', 'E2E Test Collection')
        ->set('handle', 'e2e-test-collection')
        ->set('descriptionHtml', '
Created from tests.
')
        ->call('addProduct', $product->getKey())
        ->call('save')
        ->assertSee('Collection saved');

    $collection = Collection::query()->where('handle', 'e2e-test-collection')->firstOrFail();

    expect($collection->products()->pluck('products.id')->all())->toBe([$product->getKey()]);
});
<?php

use App\Models\Cart;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

function storefrontApiCartVariant(Store $store): ProductVariant
{
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'classic-cotton-t-shirt')
        ->firstOrFail();

    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->oldest('position')
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 20,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

test('storefront cart api creates and retrieves carts for the resolved store', function (): void {
    $store = storefrontApiCartStore();

    $response = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts', ['currency' => $store->default_currency]);

    $response
        ->assertCreated()
        ->assertJsonPath('data.store_id', $store->getKey())
        ->assertJsonPath('data.currency', 'EUR')
        ->assertJsonPath('data.status', 'active')
        ->assertJsonPath('data.cart_version', 1)
        ->assertJsonPath('data.line_count', 0)
        ->assertJsonPath('data.totals.total', 0);

    $this->withHeader('Host', 'shop.test')
        ->getJson("/api/storefront/v1/carts/{$response['data']['id']}")
        ->assertOk()
        ->assertJsonPath('data.id', $response['data']['id'])
        ->assertJsonPath('data.lines', []);
});

test('storefront cart api adds updates and removes line items with version increments', function (): void {
    $store = storefrontApiCartStore();
    $variant = storefrontApiCartVariant($store);

    $cartId = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $addResponse = $this->withHeader('Host', 'shop.test')
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 2,
        ]);

    $addResponse
        ->assertCreated()
        ->assertJsonPath('data.cart_version', 2)
        ->assertJsonPath('data.line_count', 2)
        ->assertJsonPath('data.lines.0.quantity', 2)
        ->assertJsonPath('data.lines.0.line_total_amount', 4998);

    $lineId = $addResponse['data']['lines'][0]['id'];

    $this->withHeader('Host', 'shop.test')
        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'quantity' => 3,
            'cart_version' => 2,
        ])
        ->assertOk()
        ->assertJsonPath('data.cart_version', 3)
        ->assertJsonPath('data.lines.0.quantity', 3)
        ->assertJsonPath('data.totals.total', 7497);

    $this->withHeader('Host', 'shop.test')
        ->deleteJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'cart_version' => 3,
        ])
        ->assertOk()
        ->assertJsonPath('data.cart_version', 4)
        ->assertJsonPath('data.line_count', 0)
        ->assertJsonPath('data.lines', []);

    expect(Cart::withoutGlobalScopes()->findOrFail($cartId)->cart_version)->toBe(4);
});

test('storefront cart api returns the current cart on version conflicts', function (): void {
    $store = storefrontApiCartStore();
    $variant = storefrontApiCartVariant($store);

    $cartId = $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $lineId = $this->withHeader('Host', 'shop.test')
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 1,
        ])
        ->assertCreated()['data']['lines'][0]['id'];

    $this->withHeader('Host', 'shop.test')
        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
            'quantity' => 2,
            'cart_version' => 1,
        ])
        ->assertConflict()
        ->assertJsonPath('expected_cart_version', 1)
        ->assertJsonPath('current_cart_version', 2)
        ->assertJsonPath('cart.cart_version', 2)
        ->assertJsonPath('cart.lines.0.quantity', 1);
});

test('storefront cart api validates tenant and request boundaries', function (): void {
    $store = storefrontApiCartStore();

    $this->withHeader('Host', 'shop.test')
        ->postJson('/api/storefront/v1/carts', ['currency' => 'USD'])
        ->assertUnprocessable()
        ->assertJsonValidationErrors('currency');

    $this->withHeader('Host', 'shop.test')
        ->getJson('/api/storefront/v1/carts/999999')
        ->assertNotFound();

    $otherStoreCart = Cart::factory()->create(['store_id' => Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()]);

    $this->withHeader('Host', 'shop.test')
        ->getJson("/api/storefront/v1/carts/{$otherStoreCart->getKey()}")
        ->assertNotFound();
});


Output:
Chunk ID: 7a4aad
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 609
Output:
<?php

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

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartController extends Controller
{
    public function store(StoreCartRequest $request, CartService $carts): JsonResponse
    {
        $request->validated();

        return CartResource::make($this->loadCart($carts->create($this->currentStore())))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Cart $cart): CartResource
    {
        return CartResource::make($this->loadCart($cart));
    }

    private function currentStore(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}
<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CartResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        $lines = $this->whenLoaded('lines');

        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'customer_id' => $this->customer_id,
            'currency' => $this->currency,
            'status' => $this->status?->value,
            'cart_version' => $this->cart_version,
            'line_count' => $this->relationLoaded('lines') ? $this->lines->sum('quantity') : null,
            'totals' => $this->relationLoaded('lines') ? [
                'subtotal' => $this->lines->sum('line_subtotal_amount'),
                'discount' => $this->lines->sum('line_discount_amount'),
                'total' => $this->lines->sum('line_total_amount'),
                'currency' => $this->currency,
            ] : null,
            'lines' => CartLineResource::collection($lines),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


sed -n '1,240p' app/Http/Middleware/ResolveStore.php


sed -n '1,240p' app/Http/Middleware/ResolveStore.php

<?php

namespace App\Http\Middleware;

use App\Enums\StoreStatus;
use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $store = $request->is('admin*')
            ? $this->resolveAdminStore($request)
            : $this->resolveStorefrontStore($request);

        abort_unless($store, 404);

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

        if (! $request->is('admin*') && $store->status === StoreStatus::Suspended) {
            abort(503);
        }

        if ($request->is('admin*') && $store->status === StoreStatus::Suspended && ! $request->isMethodSafe()) {
            abort(403);
        }

        return $next($request);
    }

    private function resolveStorefrontStore(Request $request): ?Store
    {
        $hostname = $request->getHost();
        $cacheKey = "store_domain:{$hostname}";

        $storeId = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($hostname): ?int {
            return StoreDomain::query()
                ->where('hostname', $hostname)
                ->value('store_id');
        });

        return $storeId ? Store::query()->find($storeId) : null;
    }

    private function resolveAdminStore(Request $request): ?Store
    {
        $user = $request->user();

        if (! $user) {
            return null;
        }

        $storeId = $request->session()->get('current_store_id');

        if (! $storeId) {
            $storeId = $user->stores()->oldest('stores.id')->value('stores.id');

            if ($storeId) {
                $request->session()->put('current_store_id', $storeId);
            }
        }

        if (! $storeId || ! $user->stores()->whereKey($storeId)->exists()) {
            return null;
        }

        return Store::query()->find($storeId);
    }
}


Output:
Chunk ID: 2dcb0a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 538
Output:
<?php

namespace App\Http\Middleware;

use App\Enums\StoreStatus;
use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $store = $request->is('admin*')
            ? $this->resolveAdminStore($request)
            : $this->resolveStorefrontStore($request);

        abort_unless($store, 404);

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

        if (! $request->is('admin*') && $store->status === StoreStatus::Suspended) {
            abort(503);
        }

        if ($request->is('admin*') && $store->status === StoreStatus::Suspended && ! $request->isMethodSafe()) {
            abort(403);
        }

        return $next($request);
    }

    private function resolveStorefrontStore(Request $request): ?Store
    {
        $hostname = $request->getHost();
        $cacheKey = "store_domain:{$hostname}";

        $storeId = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($hostname): ?int {
            return StoreDomain::query()
                ->where('hostname', $hostname)
                ->value('store_id');
        });

        return $storeId ? Store::query()->find($storeId) : null;
    }

    private function resolveAdminStore(Request $request): ?Store
    {
        $user = $request->user();

        if (! $user) {
            return null;
        }

        $storeId = $request->session()->get('current_store_id');

        if (! $storeId) {
            $storeId = $user->stores()->oldest('stores.id')->value('stores.id');

            if ($storeId) {
                $request->session()->put('current_store_id', $storeId);
            }
        }

        if (! $storeId || ! $user->stores()->whereKey($storeId)->exists()) {
            return null;
        }

        return Store::query()->find($storeId);
    }
}


sed -n '1,180p' app/Models/StoreSettings.php && sed -n '1,160p' app/Models/TaxSettings.php && sed -n '1,160p' database/factories/StoreSettingsFactory.php


sed -n '1,180p' app/Models/StoreSettings.php && sed -n '1,160p' app/Models/TaxSettings.php && sed -n '1,160p' database/factories/StoreSettingsFactory.php

StoreSettingsFactory.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class StoreSettings extends Model
{
    /** @use HasFactory<\Database\Factories\StoreSettingsFactory> */
    use HasFactory;

    public $incrementing = false;

    public const CREATED_AT = null;

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'settings_json',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'settings_json' => 'array',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\TaxMode;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class TaxSettings extends Model
{
    /** @use HasFactory<\Database\Factories\TaxSettingsFactory> */
    use BelongsToStore, HasFactory;

    public $incrementing = false;

    public $timestamps = false;

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'mode',
        'provider',
        'prices_include_tax',
        'config_json',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => '{}',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'mode' => TaxMode::class,
            'prices_include_tax' => 'bool',
            'config_json' => 'array',
        ];
    }
}
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StoreSettings>
 */
class StoreSettingsFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'settings_json' => [
                'checkout' => [
                    'guest_checkout_enabled' => true,
                ],
                'notifications' => [
                    'order_confirmation' => true,
                ],
            ],
        ];
    }
}


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

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class StoreSettings extends Model
{
    /** @use HasFactory<\Database\Factories\StoreSettingsFactory> */
    use HasFactory;

    public $incrementing = false;

    public const CREATED_AT = null;

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'settings_json',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'settings_json' => 'array',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\TaxMode;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class TaxSettings extends Model
{
    /** @use HasFactory<\Database\Factories\TaxSettingsFactory> */
    use BelongsToStore, HasFactory;

    public $incrementing = false;

    public $timestamps = false;

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'mode',
        'provider',
        'prices_include_tax',
        'config_json',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => '{}',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'mode' => TaxMode::class,
            'prices_include_tax' => 'bool',
            'config_json' => 'array',
        ];
    }
}
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StoreSettings>
 */
class StoreSettingsFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'settings_json' => [
                'checkout' => [
                    'guest_checkout_enabled' => true,
                ],
                'notifications' => [
                    'order_confirmation' => true,
                ],
            ],
        ];
    }
}


sed -n '1,220p' app/Models/Concerns/BelongsToStore.php && sed -n '1,180p' app/Models/Scopes/StoreScope.php


sed -n '1,220p' app/Models/Concerns/BelongsToStore.php && sed -n '1,180p' app/Models/Scopes/StoreScope.php

StoreScope.php

<?php

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function (object $model): void {
            if ($model->store_id || ! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if ($store instanceof Store) {
                $model->store_id = $store->getKey();
            }
        });
    }
}
<?php

namespace App\Models\Scopes;

use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (! app()->bound('current_store')) {
            return;
        }

        $store = app('current_store');

        if (! $store instanceof Store) {
            return;
        }

        $builder->where($model->qualifyColumn('store_id'), $store->getKey());
    }
}


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

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function (object $model): void {
            if ($model->store_id || ! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if ($store instanceof Store) {
                $model->store_id = $store->getKey();
            }
        });
    }
}
<?php

namespace App\Models\Scopes;

use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (! app()->bound('current_store')) {
            return;
        }

        $store = app('current_store');

        if (! $store instanceof Store) {
            return;
        }

        $builder->where($model->qualifyColumn('store_id'), $store->getKey());
    }
}


sed -n '1,240p' database/seeders/DatabaseSeeder.php


sed -n '1,240p' database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            OrganizationSeeder::class,
            StoreSeeder::class,
            StoreDomainSeeder::class,
            UserSeeder::class,
            StoreUserSeeder::class,
            StoreSettingsSeeder::class,
            CollectionSeeder::class,
            ProductSeeder::class,
            ThemeSeeder::class,
            ThemeFileSeeder::class,
            ThemeSettingsSeeder::class,
            PageSeeder::class,
            NavigationMenuSeeder::class,
            NavigationItemSeeder::class,
            TaxSettingsSeeder::class,
            ShippingZoneSeeder::class,
            ShippingRateSeeder::class,
            DiscountSeeder::class,
            CustomerSeeder::class,
        ]);
    }
}


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

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            OrganizationSeeder::class,
            StoreSeeder::class,
            StoreDomainSeeder::class,
            UserSeeder::class,
            StoreUserSeeder::class,
            StoreSettingsSeeder::class,
            CollectionSeeder::class,
            ProductSeeder::class,
            ThemeSeeder::class,
            ThemeFileSeeder::class,
            ThemeSettingsSeeder::class,
            PageSeeder::class,
            NavigationMenuSeeder::class,
            NavigationItemSeeder::class,
            TaxSettingsSeeder::class,
            ShippingZoneSeeder::class,
            ShippingRateSeeder::class,
            DiscountSeeder::class,
            CustomerSeeder::class,
        ]);
    }
}


sed -n '1,180p' app/Models/InventoryItem.php && sed -n '1,120p' app/Enums/InventoryPolicy.php


sed -n '1,180p' app/Models/InventoryItem.php && sed -n '1,120p' app/Enums/InventoryPolicy.php

InventoryPolicy.php

<?php

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use InvalidArgumentException;

class InventoryItem extends Model
{
    /** @use HasFactory<\Database\Factories\InventoryItemFactory> */
    use BelongsToStore, HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'variant_id',
        'quantity_on_hand',
        'quantity_reserved',
        'policy',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'quantity_on_hand' => 0,
        'quantity_reserved' => 0,
        'policy' => 'deny',
    ];

    protected static function booted(): void
    {
        static::saving(function (InventoryItem $item): void {
            $storeId = $item->variantStoreId();

            if ($storeId === null) {
                return;
            }

            if (! $item->store_id) {
                $item->store_id = $storeId;

                return;
            }

            if ((int) $item->store_id !== $storeId) {
                throw new InvalidArgumentException('Inventory item store must match the variant product store.');
            }
        });
    }

    private function variantStoreId(): ?int
    {
        $variant = ProductVariant::withoutGlobalScopes()
            ->select(['id', 'product_id'])
            ->find($this->variant_id);

        if (! $variant instanceof ProductVariant) {
            return null;
        }

        $storeId = Product::withoutGlobalScopes()
            ->whereKey($variant->product_id)
            ->value('store_id');

        return $storeId === null ? null : (int) $storeId;
    }

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<ProductVariant, $this>
     */
    public function variant(): BelongsTo
    {
        return $this->belongsTo(ProductVariant::class, 'variant_id');
    }

    public function availableQuantity(): int
    {
        return $this->quantity_on_hand - $this->quantity_reserved;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'quantity_on_hand' => 'integer',
            'quantity_reserved' => 'integer',
            'policy' => InventoryPolicy::class,
        ];
    }
}
<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}


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

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use InvalidArgumentException;

class InventoryItem extends Model
{
    /** @use HasFactory<\Database\Factories\InventoryItemFactory> */
    use BelongsToStore, HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'variant_id',
        'quantity_on_hand',
        'quantity_reserved',
        'policy',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'quantity_on_hand' => 0,
        'quantity_reserved' => 0,
        'policy' => 'deny',
    ];

    protected static function booted(): void
    {
        static::saving(function (InventoryItem $item): void {
            $storeId = $item->variantStoreId();

            if ($storeId === null) {
                return;
            }

            if (! $item->store_id) {
                $item->store_id = $storeId;

                return;
            }

            if ((int) $item->store_id !== $storeId) {
                throw new InvalidArgumentException('Inventory item store must match the variant product store.');
            }
        });
    }

    private function variantStoreId(): ?int
    {
        $variant = ProductVariant::withoutGlobalScopes()
            ->select(['id', 'product_id'])
            ->find($this->variant_id);

        if (! $variant instanceof ProductVariant) {
            return null;
        }

        $storeId = Product::withoutGlobalScopes()
            ->whereKey($variant->product_id)
            ->value('store_id');

        return $storeId === null ? null : (int) $storeId;
    }

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<ProductVariant, $this>
     */
    public function variant(): BelongsTo
    {
        return $this->belongsTo(ProductVariant::class, 'variant_id');
    }

    public function availableQuantity(): int
    {
        return $this->quantity_on_hand - $this->quantity_reserved;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'quantity_on_hand' => 'integer',
            'quantity_reserved' => 'integer',
            'policy' => InventoryPolicy::class,
        ];
    }
}
<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}


I’m going to land search as the next vertical slice: schema and FTS5 index, a search service with product observer sync, storefront search/suggest API endpoints, and the admin search settings page. This fits the roadmap boundaries and gives the later analytics slice real search-query data to consume.


Inspect Phase 7 specs and current implementation gaps

Implement search schema/service/API/admin settings with tests

Run formatter, tests, build/browser checks as needed

Continue analytics/apps/webhooks after search commit

Explanation: Starting with search because its schema and query logs are prerequisites for search settings and analytics reporting.


Output: Plan updated


php artisan make:model SearchSettings --migration --factory --seed --no-interaction && php artisan make:model SearchQuery --migration --factory --no-interaction && php artisan make:migration create_products_fts_table --no-interaction && php artisan make:class Services/SearchService --no-interaction && php artisan make:observer ProductObserver --model=Product --no-interaction && php artisan make:controller Api/Storefront/V1/SearchController --no-interaction && php artisan make:request Api/Storefront/V1/SearchProductsRequest --no-interaction && php artisan make:request Api/Storefront/V1/SuggestProductsRequest --no-interaction && php artisan make:resource Storefront/V1/SearchProductResource --no-interaction && php artisan make:livewire Admin/Search/Settings --class --no-interaction && php artisan make:test --pest Search/SearchServiceTest --no-interaction && php artisan make:test --pest Api/StorefrontSearchApiTest --no-interaction && php artisan make:test --pest Admin/SearchSettingsTest --no-interaction


Output:
Chunk ID: 694eec
Wall time: 1.0014 seconds
Process running with session ID 67503
Original token count: 280
Output:

   INFO  Model [app/Models/SearchSettings.php] created successfully.

   INFO  Factory [database/factories/SearchSettingsFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030847_create_search_settings_table.php] created successfully.

   INFO  Seeder [database/seeders/SearchSettingsSeeder.php] created successfully.

   INFO  Model [app/Models/SearchQuery.php] created successfully.

   INFO  Factory [database/factories/SearchQueryFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030848_create_search_queries_table.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030848_create_products_fts_table.php] created successfully.

   INFO  Class [app/Services/SearchService.php] created successfully.

   INFO  Observer [app/Observers/ProductObserver.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Storefront/V1/SearchController.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php] created successfully.


php artisan make:model SearchSettings --migration --factory --seed --no-interaction && php artisan make:model SearchQuery --migration --factory --no-interaction && php artisan make:migration create_products_fts_table --no-interaction && php artisan make:class Services/SearchService --no-interaction && php artisan make:observer ProductObserver --model=Product --no-interaction && php artisan make:controller Api/Storefront/V1/SearchController --no-interaction && php artisan make:request Api/Storefront/V1/SearchProductsRequest --no-interaction && php artisan make:request Api/Storefront/V1/SuggestProductsRequest --no-interaction && php artisan make:resource Storefront/V1/SearchProductResource --no-interaction && php artisan make:livewire Admin/Search/Settings --class --no-interaction && php artisan make:test --pest Search/SearchServiceTest --no-interaction && php artisan make:test --pest Api/StorefrontSearchApiTest --no-interaction && php artisan make:test --pest Admin/SearchSettingsTest --no-interaction

   INFO  Model [app/Models/SearchSettings.php] created successfully.

   INFO  Factory [database/factories/SearchSettingsFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030847_create_search_settings_table.php] created successfully.

   INFO  Seeder [database/seeders/SearchSettingsSeeder.php] created successfully.

   INFO  Model [app/Models/SearchQuery.php] created successfully.

   INFO  Factory [database/factories/SearchQueryFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030848_create_search_queries_table.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_030848_create_products_fts_table.php] created successfully.

   INFO  Class [app/Services/SearchService.php] created successfully.

   INFO  Observer [app/Observers/ProductObserver.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Storefront/V1/SearchController.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php] created successfully.

   INFO  Resource [app/Http/Resources/Storefront/V1/SearchProductResource.php] created successfully.

   INFO  Livewire component [app/Livewire/Admin/Search/Settings.php] created successfully.

   INFO  Test [tests/Feature/Search/SearchServiceTest.php] created successfully.

   INFO  Test [tests/Feature/Api/StorefrontSearchApiTest.php] created successfully.

   INFO  Test [tests/Feature/Admin/SearchSettingsTest.php] created successfully.


Output:
Chunk ID: 54cc3d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 142
Output:

   INFO  Request [app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php] created successfully.

   INFO  Resource [app/Http/Resources/Storefront/V1/SearchProductResource.php] created successfully.

   INFO  Livewire component [app/Livewire/Admin/Search/Settings.php] created successfully.

   INFO  Test [tests/Feature/Search/SearchServiceTest.php] created successfully.

   INFO  Test [tests/Feature/Api/StorefrontSearchApiTest.php] created successfully.

   INFO  Test [tests/Feature/Admin/SearchSettingsTest.php] created successfully.


The scaffolded files are in place. I’m patching them now to match the existing conventions: store-scoped models, FTS-safe querying, Form Requests for API validation, and Livewire admin actions with policy checks.


sed -n '1,220p' app/Models/SearchSettings.php && sed -n '1,220p' app/Models/SearchQuery.php


sed -n '1,200p' database/migrations/2026_05_04_030847_create_search_settings_table.php && sed -n '1,220p' database/migrations/2026_05_04_030848_create_search_queries_table.php && sed -n '1,200p' database/migrations/2026_05_04_030848_create_products_fts_table.php


sed -n '1,200p' app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php && sed -n '1,200p' app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php && sed -n '1,200p' app/Http/Resources/Storefront/V1/SearchProductResource.php


sed -n '1,220p' app/Livewire/Admin/Search/Settings.php && sed -n '1,220p' resources/views/livewire/admin/search/settings.blade.php && sed -n '1,140p' app/Observers/ProductObserver.php && sed -n '1,160p' app/Services/SearchService.php


sed -n '1,220p' app/Models/SearchSettings.php && sed -n '1,220p' app/Models/SearchQuery.php

SearchQuery.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SearchSettings extends Model
{
    /** @use HasFactory<\Database\Factories\SearchSettingsFactory> */
    use HasFactory;
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SearchQuery extends Model
{
    /** @use HasFactory<\Database\Factories\SearchQueryFactory> */
    use HasFactory;
}


Output:
Chunk ID: 2d92c9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 126
Output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SearchSettings extends Model
{
    /** @use HasFactory<\Database\Factories\SearchSettingsFactory> */
    use HasFactory;
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class SearchQuery extends Model
{
    /** @use HasFactory<\Database\Factories\SearchQueryFactory> */
    use HasFactory;
}


sed -n '1,200p' database/migrations/2026_05_04_030847_create_search_settings_table.php && sed -n '1,220p' database/migrations/2026_05_04_030848_create_search_queries_table.php && sed -n '1,200p' database/migrations/2026_05_04_030848_create_products_fts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_settings', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_settings');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_queries', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_queries');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products_fts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products_fts');
    }
};


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

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_settings', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_settings');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_queries', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_queries');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products_fts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products_fts');
    }
};


sed -n '1,200p' app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php && sed -n '1,200p' app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php && sed -n '1,200p' app/Http/Resources/Storefront/V1/SearchProductResource.php

SearchProductResource.php

<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class SearchProductsRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class SuggestProductsRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}
<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class SearchProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}


Output:
Chunk ID: 27c693
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 387
Output:
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class SearchProductsRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class SuggestProductsRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}
<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class SearchProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}


sed -n '1,220p' app/Livewire/Admin/Search/Settings.php && sed -n '1,220p' resources/views/livewire/admin/search/settings.blade.php && sed -n '1,140p' app/Observers/ProductObserver.php && sed -n '1,160p' app/Services/SearchService.php

SearchService.php

<?php

namespace App\Livewire\Admin\Search;

use Livewire\Component;

class Settings extends Component
{
    public function render()
    {
        return view('livewire.admin.search.settings');
    }
}
<div>
    {{-- The best way to take care of the future is to take care of the present moment. - Thich Nhat Hanh --}}
</div>
<?php

namespace App\Observers;

use App\Models\Product;

class ProductObserver
{
    /**
     * Handle the Product "created" event.
     */
    public function created(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "updated" event.
     */
    public function updated(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "deleted" event.
     */
    public function deleted(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "restored" event.
     */
    public function restored(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "force deleted" event.
     */
    public function forceDeleted(Product $product): void
    {
        //
    }
}
<?php

namespace App\Services;

class SearchService
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


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

namespace App\Livewire\Admin\Search;

use Livewire\Component;

class Settings extends Component
{
    public function render()
    {
        return view('livewire.admin.search.settings');
    }
}
<div>
    {{-- The best way to take care of the future is to take care of the present moment. - Thich Nhat Hanh --}}
</div>
<?php

namespace App\Observers;

use App\Models\Product;

class ProductObserver
{
    /**
     * Handle the Product "created" event.
     */
    public function created(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "updated" event.
     */
    public function updated(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "deleted" event.
     */
    public function deleted(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "restored" event.
     */
    public function restored(Product $product): void
    {
        //
    }

    /**
     * Handle the Product "force deleted" event.
     */
    public function forceDeleted(Product $product): void
    {
        //
    }
}
<?php

namespace App\Services;

class SearchService
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


sed -n '1,160p' database/factories/SearchSettingsFactory.php && sed -n '1,160p' database/factories/SearchQueryFactory.php && sed -n '1,120p' database/seeders/SearchSettingsSeeder.php


sed -n '1,120p' tests/Feature/Search/SearchServiceTest.php && sed -n '1,120p' tests/Feature/Api/StorefrontSearchApiTest.php && sed -n '1,120p' tests/Feature/Admin/SearchSettingsTest.php


sed -n '1,240p' resources/views/layouts/app/sidebar.blade.php


sed -n '1,240p' resources/views/layouts/app/sidebar.blade.php

<!DOCTYPE html>
getLocale()) }}" class="dark">

        @include('partials.head')

        <flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:sidebar.header>
                <x-app-logo :sidebar="true" href="{{ route('admin.dashboard') }}" wire:navigate />
                <flux:sidebar.collapse class="lg:hidden" />
            </flux:sidebar.header>

            <flux:sidebar.nav>
                <flux:sidebar.group :heading="__('Platform')" class="grid">
                    <flux:sidebar.item icon="home" :href="route('admin.dashboard')" :current="request()->routeIs('admin.dashboard')" wire:navigate>
                        {{ __('Dashboard') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Products')" class="grid">
                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
                        {{ __('Products') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="rectangle-stack" :href="route('admin.collections.index')" :current="request()->routeIs('admin.collections.*')" wire:navigate>
                        {{ __('Collections') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="archive-box" :href="route('admin.inventory.index')" :current="request()->routeIs('admin.inventory.*')" wire:navigate>
                        {{ __('Inventory') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Operations')" class="grid">
                    <flux:sidebar.item icon="shopping-bag" :href="route('admin.orders.index')" :current="request()->routeIs('admin.orders.*')" wire:navigate>
                        {{ __('Orders') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="users" :href="route('admin.customers.index')" :current="request()->routeIs('admin.customers.*')" wire:navigate>
                        {{ __('Customers') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="tag" :href="route('admin.discounts.index')" :current="request()->routeIs('admin.discounts.*')" wire:navigate>
                        {{ __('Discounts') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Storefront')" class="grid">
                    <flux:sidebar.item icon="document-text" :href="route('admin.pages.index')" :current="request()->routeIs('admin.pages.*')" wire:navigate>
                        {{ __('Pages') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="bars-3" :href="route('admin.navigation.index')" :current="request()->routeIs('admin.navigation.*')" wire:navigate>
                        {{ __('Navigation') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="paint-brush" :href="route('admin.themes.index')" :current="request()->routeIs('admin.themes.*')" wire:navigate>
                        {{ __('Themes') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Configuration')" class="grid">
                    <flux:sidebar.item icon="cog-6-tooth" :href="route('admin.settings.index')" :current="request()->routeIs('admin.settings.*')" wire:navigate>
                        {{ __('Settings') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>
            </flux:sidebar.nav>

            <flux:spacer />

            <flux:sidebar.nav>
                <flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
                    {{ __('Repository') }}
                </flux:sidebar.item>

                <flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
                    {{ __('Documentation') }}
                </flux:sidebar.item>
            </flux:sidebar.nav>

            <x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
        </flux:sidebar>

        <flux:header class="lg:hidden">
            <flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />

            <flux:spacer />

            <flux:dropdown position="top" align="end">
                <flux:profile
                    :initials="auth()->user()->initials()"
                    icon-trailing="chevron-down"
                />

                <flux:menu>
                    <flux:menu.radio.group>

                                <flux:avatar
                                    :name="auth()->user()->name"
                                    :initials="auth()->user()->initials()"
                                />

                                    <flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
                                    <flux:text class="truncate">{{ auth()->user()->email }}</flux:text>

                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <flux:menu.radio.group>
                        <flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
                            {{ __('Settings') }}
                        </flux:menu.item>
                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <form method="POST" action="{{ route('logout') }}" class="w-full">
                        @csrf
                        <flux:menu.item
                            as="button"
                            type="submit"
                            icon="arrow-right-start-on-rectangle"
                            class="w-full cursor-pointer"
                            data-test="logout-button"
                        >
                            {{ __('Log Out') }}
                        </flux:menu.item>
                    </form>
                </flux:menu>
            </flux:dropdown>
        </flux:header>

        {{ $slot }}

        @fluxScripts


sed -n '1,120p' tests/Feature/Search/SearchServiceTest.php && sed -n '1,120p' tests/Feature/Api/StorefrontSearchApiTest.php && sed -n '1,120p' tests/Feature/Admin/SearchSettingsTest.php

SearchSettingsTest.php

<?php

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

    $response->assertStatus(200);
});
<?php

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

    $response->assertStatus(200);
});
<?php

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

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