sed -n '1,140p' app/Http/Middleware/AuthenticateAdminApi.php

<?php

namespace App\Http\Middleware;

use App\Enums\AppInstallationStatus;
use App\Models\OauthToken;
use App\Models\Store;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AuthenticateAdminApi
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$abilities): Response
    {
        $store = $this->storeFromRoute($request);

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

        if ($request->user() !== null) {
            if ($request->user()->stores()->whereKey($store->getKey())->exists()) {
                return $next($request);
            }

            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $plainTextToken = $request->bearerToken();

        if (! is_string($plainTextToken) || $plainTextToken === '') {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $token = OauthToken::query()
            ->with('installation')
            ->where('access_token_hash', hash('sha256', $plainTextToken))
            ->first();

        if (! $token instanceof OauthToken || $token->isExpired()) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $installation = $token->installation;

        if ($installation === null
            || $installation->status !== AppInstallationStatus::Active
            || (int) $installation->store_id !== $store->getKey()) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        if (! $this->hasAbilities($token, $abilities)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $token->forceFill(['last_used_at' => now()])->save();

        $request->attributes->set('admin_api_oauth_token', $token);
        app()->instance('admin_api_oauth_token', $token);

        return $next($request);
    }

    private function storeFromRoute(Request $request): Store
    {
        $store = $request->route('store');

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

        return Store::query()->findOrFail($store);
    }

    /**
     * @param  list<string>  $abilities
     */
    private function hasAbilities(OauthToken $token, array $abilities): bool
    {
        if ($abilities === []) {
            return true;
        }

        $tokenAbilities = $token->abilities_json ?? [];

        if (in_array('*', $tokenAbilities, true)) {
            return true;
        }

        foreach ($abilities as $ability) {
            if (! in_array($ability, $tokenAbilities, true)) {
                return false;
            }
        }

        return true;
    }
}


rg --files database/migrations | rg 'product|inventory|collection_products|variant_option'

Question: product|inventory|collection_products|variant_option


Output:
Chunk ID: ae6913
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 161
Output:
database/migrations/2026_05_04_030848_create_products_fts_table.php
database/migrations/2026_05_03_213900_create_variant_option_values_table.php
database/migrations/2026_05_03_213901_create_inventory_items_table.php
database/migrations/2026_05_03_213904_create_product_media_table.php
database/migrations/2026_05_03_213856_create_products_table.php
database/migrations/2026_05_03_213858_create_product_option_values_table.php
database/migrations/2026_05_03_213857_create_product_options_table.php
database/migrations/2026_05_03_213903_create_collection_products_table.php
database/migrations/2026_05_03_213859_create_product_variants_table.php


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

namespace App\Http\Middleware;

use App\Enums\AppInstallationStatus;
use App\Models\OauthToken;
use App\Models\Store;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AuthenticateAdminApi
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$abilities): Response
    {
        $store = $this->storeFromRoute($request);

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

        if ($request->user() !== null) {
            if ($request->user()->stores()->whereKey($store->getKey())->exists()) {
                return $next($request);
            }

            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $plainTextToken = $request->bearerToken();

        if (! is_string($plainTextToken) || $plainTextToken === '') {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $token = OauthToken::query()
            ->with('installation')
            ->where('access_token_hash', hash('sha256', $plainTextToken))
            ->first();

        if (! $token instanceof OauthToken || $token->isExpired()) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $installation = $token->installation;

        if ($installation === null
            || $installation->status !== AppInstallationStatus::Active
            || (int) $installation->store_id !== $store->getKey()) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        if (! $this->hasAbilities($token, $abilities)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $token->forceFill(['last_used_at' => now()])->save();

        $request->attributes->set('admin_api_oauth_token', $token);
        app()->instance('admin_api_oauth_token', $token);

        return $next($request);
    }

    private function storeFromRoute(Request $request): Store
    {
        $store = $request->route('store');

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

        return Store::query()->findOrFail($store);
    }

    /**
     * @param  list<string>  $abilities
     */
    private function hasAbilities(OauthToken $token, array $abilities): bool
    {
        if ($abilities === []) {
            return true;
        }

        $tokenAbilities = $token->abilities_json ?? [];

        if (in_array('*', $tokenAbilities, true)) {
            return true;
        }

        foreach ($abilities as $ability) {
            if (! in_array($ability, $tokenAbilities, true)) {
                return false;
            }
        }

        return true;
    }
}


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


sed -n '1,220p' database/migrations/2026_05_03_213857_create_product_options_table.php


sed -n '1,220p' database/migrations/2026_05_03_213858_create_product_option_values_table.php


sed -n '1,240p' database/migrations/2026_05_03_213859_create_product_variants_table.php


sed -n '1,220p' database/migrations/2026_05_03_213901_create_inventory_items_table.php


sed -n '1,180p' database/migrations/2026_05_03_213900_create_variant_option_values_table.php


sed -n '1,220p' database/migrations/2026_05_03_213857_create_product_options_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('product_options', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->unsignedInteger('position')->default(0);

            $table->index('product_id');
            $table->unique(['product_id', 'position']);
        });
    }

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


sed -n '1,220p' database/migrations/2026_05_03_213856_create_products_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');
    }
};


Output:
Chunk ID: 8c7ef3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 338
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');
    }
};


Output:
Chunk ID: 5944c1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 197
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('product_options', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->unsignedInteger('position')->default(0);

            $table->index('product_id');
            $table->unique(['product_id', 'position']);
        });
    }

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


sed -n '1,220p' database/migrations/2026_05_03_213858_create_product_option_values_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('product_option_values', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_option_id')->constrained()->cascadeOnDelete();
            $table->string('value');
            $table->unsignedInteger('position')->default(0);

            $table->index('product_option_id');
            $table->unique(['product_option_id', 'position']);
        });
    }

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


Output:
Chunk ID: 7f9e6f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 206
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('product_option_values', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_option_id')->constrained()->cascadeOnDelete();
            $table->string('value');
            $table->unsignedInteger('position')->default(0);

            $table->index('product_option_id');
            $table->unique(['product_option_id', 'position']);
        });
    }

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


sed -n '1,240p' database/migrations/2026_05_03_213859_create_product_variants_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('product_variants', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('sku')->nullable();
            $table->string('barcode')->nullable();
            $table->integer('price_amount')->default(0);
            $table->integer('compare_at_amount')->nullable();
            $table->string('currency', 3)->default('USD');
            $table->integer('weight_g')->nullable();
            $table->boolean('requires_shipping')->default(true);
            $table->boolean('is_default')->default(false);
            $table->unsignedInteger('position')->default(0);
            $table->enum('status', ['active', 'archived'])->default('active');
            $table->timestamps();

            $table->index('product_id');
            $table->index('sku');
            $table->index('barcode');
            $table->index(['product_id', 'position']);
            $table->index(['product_id', 'is_default']);
        });
    }

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


Output:
Chunk ID: 8ad7a8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 362
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('product_variants', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->string('sku')->nullable();
            $table->string('barcode')->nullable();
            $table->integer('price_amount')->default(0);
            $table->integer('compare_at_amount')->nullable();
            $table->string('currency', 3)->default('USD');
            $table->integer('weight_g')->nullable();
            $table->boolean('requires_shipping')->default(true);
            $table->boolean('is_default')->default(false);
            $table->unsignedInteger('position')->default(0);
            $table->enum('status', ['active', 'archived'])->default('active');
            $table->timestamps();

            $table->index('product_id');
            $table->index('sku');
            $table->index('barcode');
            $table->index(['product_id', 'position']);
            $table->index(['product_id', 'is_default']);
        });
    }

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


sed -n '1,220p' database/migrations/2026_05_03_213901_create_inventory_items_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('inventory_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->unique()->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity_on_hand')->default(0);
            $table->integer('quantity_reserved')->default(0);
            $table->enum('policy', ['deny', 'continue'])->default('deny');

            $table->index('store_id');
        });
    }

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


sed -n '1,180p' database/migrations/2026_05_03_213900_create_variant_option_values_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('variant_option_values', function (Blueprint $table) {
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete();

            $table->primary(['variant_id', 'product_option_value_id']);
            $table->index('product_option_value_id');
        });
    }

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


Output:
Chunk ID: 234fb2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 234
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('inventory_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('variant_id')->unique()->constrained('product_variants')->cascadeOnDelete();
            $table->integer('quantity_on_hand')->default(0);
            $table->integer('quantity_reserved')->default(0);
            $table->enum('policy', ['deny', 'continue'])->default('deny');

            $table->index('store_id');
        });
    }

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


Output:
Chunk ID: ce3819
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 204
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('variant_option_values', function (Blueprint $table) {
            $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete();
            $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete();

            $table->primary(['variant_id', 'product_option_value_id']);
            $table->index('product_option_value_id');
        });
    }

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


Audit progress notes and specs for remaining incomplete/deferred items

Implement missing admin product write API routes from Spec 02

Run focused catalog API and affected feature tests

Update progress notes and commit the product API slice

Continue final completion audit for any remaining real gaps

Explanation: The audit found a concrete Spec 02 gap: product create/update/delete/media presign routes were missing while product read routes existed.


Output: Plan updated


rg -n "temporaryUploadUrl|serve|local" config bootstrap app tests -S


/bin/zsh
-lc
sed -n '1,220p' config/filesystems.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Filesystem Disk
    |--------------------------------------------------------------------------
    |
    | Here you may specify the default filesystem disk that should be used
    | by the framework. The "local" disk, as well as a variety of cloud
    | based disks are available to your application for file storage.
    |
    */

    'default' => env('FILESYSTEM_DISK', 'local'),

    /*
    |--------------------------------------------------------------------------
    | Filesystem Disks
    |--------------------------------------------------------------------------
    |
    | Below you may configure as many filesystem disks as necessary, and you
    | may even configure multiple disks for the same driver. Examples for
    | most supported storage drivers are configured here for reference.
    |
    | Supported drivers: "local", "ftp", "sftp", "s3"
    |
    */

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app/private'),
            'serve' => true,
            'throw' => false,
            'report' => false,
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
            'visibility' => 'public',
            'throw' => false,
            'report' => false,
        ],

        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
            'throw' => false,
            'report' => false,
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Symbolic Links
    |--------------------------------------------------------------------------
    |
    | Here you may configure the symbolic links that will be created when the
    | `storage:link` Artisan command is executed. The array keys should be
    | the locations of the links and the values should be their targets.
    |
    */

    'links' => [
        public_path('storage') => storage_path('app/public'),
    ],

];


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

return [

    /*
    |--------------------------------------------------------------------------
    | Default Filesystem Disk
    |--------------------------------------------------------------------------
    |
    | Here you may specify the default filesystem disk that should be used
    | by the framework. The "local" disk, as well as a variety of cloud
    | based disks are available to your application for file storage.
    |
    */

    'default' => env('FILESYSTEM_DISK', 'local'),

    /*
    |--------------------------------------------------------------------------
    | Filesystem Disks
    |--------------------------------------------------------------------------
    |
    | Below you may configure as many filesystem disks as necessary, and you
    | may even configure multiple disks for the same driver. Examples for
    | most supported storage drivers are configured here for reference.
    |
    | Supported drivers: "local", "ftp", "sftp", "s3"
    |
    */

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app/private'),
            'serve' => true,
            'throw' => false,
            'report' => false,
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
            'visibility' => 'public',
            'throw' => false,
            'report' => false,
        ],

        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
            'throw' => false,
            'report' => false,
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Symbolic Links
    |--------------------------------------------------------------------------
    |
    | Here you may configure the symbolic links that will be created when the
    | `storage:link` Artisan command is executed. The array keys should be
    | the locations of the links and the values should be their targets.
    |
    */

    'links' => [
        public_path('storage') => storage_path('app/public'),
    ],

];


rg -n "temporaryUploadUrl|serve|local" config bootstrap app tests -S

rg -n 'temporaryUploadUrl|serve|local' config bootstrap app tests -S

Question: temporaryUploadUrl|serve|local

config/filesystems.php:11:    | by the framework. The "local" disk, as well as a variety of cloud
config/filesystems.php:16:    'default' => env('FILESYSTEM_DISK', 'local'),
config/filesystems.php:27:    | Supported drivers: "local", "ftp", "sftp", "s3"
config/filesystems.php:33:        'local' => [
config/filesystems.php:34:            'driver' => 'local',
config/filesystems.php:36:            'serve' => true,
config/filesystems.php:42:            'driver' => 'local',
config/filesystems.php:44:            'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
tests/Feature/Orders/OrderServiceTest.php:70:            'quantity_reserved' => 0,
tests/Feature/Orders/OrderServiceTest.php:182:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/OrderServiceTest.php:265:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
tests/Feature/Orders/OrderServiceTest.php:268:test('bank transfer completion creates a pending order and keeps inventory reserved', function () {
tests/Feature/Orders/OrderServiceTest.php:284:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Orders/OrderServiceTest.php:290:test('payment failures release reserved inventory and allow checkout retry', function () {
tests/Feature/Orders/OrderServiceTest.php:302:        ->and($inventory->quantity_reserved)->toBe(0)
config/mail.php:49:            'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
config/queue.php:49:            'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
tests/Feature/Orders/BankTransferOrderTest.php:50:            'quantity_reserved' => 2,
tests/Feature/Orders/BankTransferOrderTest.php:80:test('confirming bank transfer payment captures payment and commits reserved inventory', function () {
tests/Feature/Orders/BankTransferOrderTest.php:92:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/BankTransferOrderTest.php:123:        ->and($oldInventory->quantity_reserved)->toBe(0)
tests/Feature/Orders/BankTransferOrderTest.php:125:        ->and($newInventory->quantity_reserved)->toBe(2);
tests/Feature/Orders/RefundServiceTest.php:41:            'quantity_reserved' => 0,
config/session.php:165:    | to the server if the browser has a HTTPS connection. This will keep
config/app.php:55:    'url' => env('APP_URL', 'http://localhost'),
config/app.php:75:    | The application locale determines the default locale that will be used
config/app.php:76:    | by Laravel's translation / localization methods. This option can be
config/app.php:77:    | set to any locale for which you plan to have translation strings.
config/app.php:81:    'locale' => env('APP_LOCALE', 'en'),
config/app.php:83:    'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
config/app.php:85:    'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
app/Services/VariantMatrixService.php:91:                    'quantity_reserved' => 0,
app/Services/VariantMatrixService.php:113:                'quantity_reserved' => 0,
app/Services/VariantMatrixService.php:185:                'quantity_reserved' => 0,
tests/Feature/Api/StorefrontCheckoutApiTest.php:41:            'quantity_reserved' => 0,
tests/Feature/Api/StorefrontCheckoutApiTest.php:141:        ->and($inventory->quantity_reserved)->toBe(2);
config/cache.php:66:            'servers' => [
app/Services/SearchService.php:328:                            ->whereRaw('inventory_items.quantity_on_hand > inventory_items.quantity_reserved')
app/Services/OrderExportService.php:35:            Storage::disk('local')->put($storageKey, $csv);
tests/Feature/Api/AdminStoreSettingsApiTest.php:52:            'default_locale' => 'de',
tests/Feature/Api/AdminStoreSettingsApiTest.php:69:        ->assertJsonPath('data.default_locale', 'de')
tests/Feature/Api/AdminStoreSettingsApiTest.php:123:            'default_locale' => 'es',
tests/Feature/Api/AdminStoreSettingsApiTest.php:127:        ->assertJsonValidationErrors(['default_currency', 'default_locale', 'timezone']);
config/database.php:104:            'host' => env('DB_HOST', 'localhost'),
config/database.php:113:            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
tests/Feature/Seeders/SeededOrderDataTest.php:104:        ->and($inventory->quantity_reserved)->toBe($bankTransferLine->quantity);
tests/Feature/Api/StorefrontOrderApiTest.php:40:            'quantity_reserved' => 0,
tests/Feature/Api/StorefrontOrderApiTest.php:146:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Storefront/OrderViewsTest.php:129:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Api/AdminOrderApiTest.php:61:            'quantity_reserved' => 0,
tests/Feature/Checkout/CheckoutServiceTest.php:54:            'quantity_reserved' => 0,
tests/Feature/Checkout/CheckoutServiceTest.php:106:        ->and(InventoryItem::withoutGlobalSco…5379 chars truncated…hp:181:            $this->releaseReservedOrderInventory($order);
app/Services/OrderService.php:355:    private function commitReservedInventory(Checkout $checkout): void
app/Services/OrderService.php:362:    private function commitReservedOrderInventory(Order $order): void
app/Services/OrderService.php:371:    private function releaseReservedOrderInventory(Order $order): void
app/Services/InventoryService.php:24:    public function reserve(InventoryItem $item, int $quantity): void
app/Services/InventoryService.php:36:                'quantity_reserved' => $locked->quantity_reserved + $quantity,
app/Services/InventoryService.php:48:            if ($locked->quantity_reserved < $quantity) {
app/Services/InventoryService.php:49:                throw new InvalidArgumentException('Cannot release more inventory than is reserved.');
app/Services/InventoryService.php:53:                'quantity_reserved' => $locked->quantity_reserved - $quantity,
app/Services/InventoryService.php:65:            if ($locked->quantity_reserved < $quantity) {
app/Services/InventoryService.php:66:                throw new InvalidArgumentException('Cannot commit more inventory than is reserved.');
app/Services/InventoryService.php:71:                'quantity_reserved' => $locked->quantity_reserved - $quantity,
app/Services/InventoryService.php:91:        return $item->quantity_on_hand - $item->quantity_reserved;
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:31:            'default_locale' => ['sometimes', 'required', Rule::in(array_keys($this->localeOptions()))],
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:69:            'default_locale',
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:136:    private function localeOptions(): array
app/Providers/AppServiceProvider.php:21:use App\Observers\ProductObserver;
app/Providers/AppServiceProvider.php:117:        Product::observe(ProductObserver::class);
app/Models/InventoryItem.php:26:        'quantity_reserved',
app/Models/InventoryItem.php:35:        'quantity_reserved' => 0,
app/Models/InventoryItem.php:95:        return $this->quantity_on_hand - $this->quantity_reserved;
app/Models/InventoryItem.php:105:            'quantity_reserved' => 'integer',
app/Models/ProductVariant.php:90:                'quantity_reserved' => 0,
app/Http/Resources/Admin/V1/StoreSettingsResource.php:24:            'default_locale' => $this->default_locale,
app/Http/Resources/Admin/V1/ProductResource.php:69:                        'quantity_reserved' => $variant->inventoryItem?->quantity_reserved ?? 0,
app/Http/Resources/Admin/V1/DataExportResource.php:42:        if (! Storage::disk('local')->exists($this->storage_key)) {
app/Http/Resources/Admin/V1/DataExportResource.php:46:        return 'data:text/csv;charset=utf-8,'.rawurlencode(Storage::disk('local')->get($this->storage_key));
app/Livewire/Storefront/Collections/Show.php:78:                        ->orWhereColumn('quantity_on_hand', '>', 'quantity_reserved');
app/Livewire/Admin/Themes/Editor.php:65:        $this->fileContents = Storage::disk('local')->exists($file->storage_key)
app/Livewire/Admin/Themes/Editor.php:66:            ? Storage::disk('local')->get($file->storage_key)
app/Livewire/Admin/Themes/Editor.php:81:        Storage::disk('local')->put($file->storage_key, $this->fileContents);
app/Livewire/Storefront/Checkout/Show.php:203:            $this->step = 'reserved';
app/Livewire/Storefront/Checkout/Show.php:422:            CheckoutStatus::PaymentSelected => 'reserved',
app/Livewire/Admin/Themes/Index.php:81:                    $contents = Storage::disk('local')->exists($file->storage_key)
app/Livewire/Admin/Themes/Index.php:82:                        ? Storage::disk('local')->get($file->storage_key)
app/Livewire/Admin/Themes/Index.php:86:                    Storage::disk('local')->put($storageKey, $contents);
app/Livewire/Admin/Products/Form.php:504:                    'quantity_reserved' => 0,
app/Livewire/Admin/Navigation/Index.php:297:            ->groupBy(fn (array $item): string => $item['parent_key'] ?? 'root', preserveKeys: true);
app/Livewire/Admin/Settings/Index.php:54:        $this->defaultLocale = $store->default_locale;
app/Livewire/Admin/Settings/Index.php:70:            'defaultLocale' => ['required', Rule::in(array_keys($this->localeOptions()))],
app/Livewire/Admin/Settings/Index.php:80:            'default_locale' => $validated['defaultLocale'],
app/Livewire/Admin/Settings/Index.php:191:    public function localeOptions(): array
app/Livewire/Admin/Settings/Index.php:224:            'localeOptions' => $this->localeOptions(),
app/Livewire/Admin/Inventory/Index.php:42:            ->when($this->stockFilter === 'low', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) BETWEEN 1 AND 10'))
app/Livewire/Admin/Inventory/Index.php:43:            ->when($this->stockFilter === 'out', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) <= 0'))


Output:
Chunk ID: d77455
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3845
Output:
config/filesystems.php:11:    | by the framework. The "local" disk, as well as a variety of cloud
config/filesystems.php:16:    'default' => env('FILESYSTEM_DISK', 'local'),
config/filesystems.php:27:    | Supported drivers: "local", "ftp", "sftp", "s3"
config/filesystems.php:33:        'local' => [
config/filesystems.php:34:            'driver' => 'local',
config/filesystems.php:36:            'serve' => true,
config/filesystems.php:42:            'driver' => 'local',
config/filesystems.php:44:            'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
tests/Feature/Orders/OrderServiceTest.php:70:            'quantity_reserved' => 0,
tests/Feature/Orders/OrderServiceTest.php:182:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/OrderServiceTest.php:265:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
tests/Feature/Orders/OrderServiceTest.php:268:test('bank transfer completion creates a pending order and keeps inventory reserved', function () {
tests/Feature/Orders/OrderServiceTest.php:284:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Orders/OrderServiceTest.php:290:test('payment failures release reserved inventory and allow checkout retry', function () {
tests/Feature/Orders/OrderServiceTest.php:302:        ->and($inventory->quantity_reserved)->toBe(0)
config/mail.php:49:            'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
config/queue.php:49:            'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
tests/Feature/Orders/BankTransferOrderTest.php:50:            'quantity_reserved' => 2,
tests/Feature/Orders/BankTransferOrderTest.php:80:test('confirming bank transfer payment captures payment and commits reserved inventory', function () {
tests/Feature/Orders/BankTransferOrderTest.php:92:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/BankTransferOrderTest.php:123:        ->and($oldInventory->quantity_reserved)->toBe(0)
tests/Feature/Orders/BankTransferOrderTest.php:125:        ->and($newInventory->quantity_reserved)->toBe(2);
tests/Feature/Orders/RefundServiceTest.php:41:            'quantity_reserved' => 0,
config/session.php:165:    | to the server if the browser has a HTTPS connection. This will keep
config/app.php:55:    'url' => env('APP_URL', 'http://localhost'),
config/app.php:75:    | The application locale determines the default locale that will be used
config/app.php:76:    | by Laravel's translation / localization methods. This option can be
config/app.php:77:    | set to any locale for which you plan to have translation strings.
config/app.php:81:    'locale' => env('APP_LOCALE', 'en'),
config/app.php:83:    'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
config/app.php:85:    'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
app/Services/VariantMatrixService.php:91:                    'quantity_reserved' => 0,
app/Services/VariantMatrixService.php:113:                'quantity_reserved' => 0,
app/Services/VariantMatrixService.php:185:                'quantity_reserved' => 0,
tests/Feature/Api/StorefrontCheckoutApiTest.php:41:            'quantity_reserved' => 0,
tests/Feature/Api/StorefrontCheckoutApiTest.php:141:        ->and($inventory->quantity_reserved)->toBe(2);
config/cache.php:66:            'servers' => [
app/Services/SearchService.php:328:                            ->whereRaw('inventory_items.quantity_on_hand > inventory_items.quantity_reserved')
app/Services/OrderExportService.php:35:            Storage::disk('local')->put($storageKey, $csv);
tests/Feature/Api/AdminStoreSettingsApiTest.php:52:            'default_locale' => 'de',
tests/Feature/Api/AdminStoreSettingsApiTest.php:69:        ->assertJsonPath('data.default_locale', 'de')
tests/Feature/Api/AdminStoreSettingsApiTest.php:123:            'default_locale' => 'es',
tests/Feature/Api/AdminStoreSettingsApiTest.php:127:        ->assertJsonValidationErrors(['default_currency', 'default_locale', 'timezone']);
config/database.php:104:            'host' => env('DB_HOST', 'localhost'),
config/database.php:113:            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
tests/Feature/Seeders/SeededOrderDataTest.php:104:        ->and($inventory->quantity_reserved)->toBe($bankTransferLine->quantity);
tests/Feature/Api/StorefrontOrderApiTest.php:40:            'quantity_reserved' => 0,
tests/Feature/Api/StorefrontOrderApiTest.php:146:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Storefront/OrderViewsTest.php:129:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Api/AdminOrderApiTest.php:61:            'quantity_reserved' => 0,
tests/Feature/Checkout/CheckoutServiceTest.php:54:            'quantity_reserved' => 0,
tests/Feature/Checkout/CheckoutServiceTest.php:106:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);
tests/Feature/Checkout/CheckoutServiceTest.php:109:    expect(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);
tests/Feature/Checkout/CheckoutServiceTest.php:113:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
tests/Feature/Checkout/CheckoutServiceTest.php:175:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
tests/Feature/Checkout/CheckoutServiceTest.php:206:        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
tests/Feature/Storefront/CartCheckoutUiTest.php:93:        'quantity_reserved' => 0,
tests/Feature/Storefront/CartCheckoutUiTest.php:194:        ->assertSet('step', 'reserved');
tests/Feature/Storefront/CartCheckoutUiTest.php:202:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Api/AdminThemeApiTest.php:124:        ->and(Storage::disk('local')->get($hero->storage_key))->toBe('
API Hero
');
tests/Feature/Search/SearchServiceTest.php:59:test('search service keeps the FTS index synchronized through product observer events', function (): void {
tests/Feature/Api/StorefrontCartApiTest.php:39:            'quantity_reserved' => 0,
app/Services/ProductService.php:183:                'quantity_reserved' => 0,
tests/Feature/Api/AdminOrderExportApiTest.php:56:    Storage::fake('local');
tests/Feature/Api/AdminOrderExportApiTest.php:92:    Storage::disk('local')->assertExists($export->storage_key);
tests/Feature/Api/AdminOrderExportApiTest.php:112:    Storage::fake('local');
tests/Feature/Cart/CartServiceTest.php:40:            'quantity_reserved' => 0,
app/Observers/ProductObserver.php:3:namespace App\Observers;
app/Observers/ProductObserver.php:8:class ProductObserver
tests/Feature/Admin/OrderManagementTest.php:60:            'quantity_reserved' => 0,
tests/Feature/Admin/OrderManagementTest.php:163:        ->update(['quantity_reserved' => $line->quantity]);
tests/Feature/Admin/OrderManagementTest.php:176:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Admin/SettingsManagementTest.php:95:        ->and($store->default_locale)->toBe('de')
tests/Browser/Admin/OrderManagementTest.php:63:            'quantity_reserved' => $bankLine->quantity,
tests/Feature/Admin/ContentManagementTest.php:219:        ->and(Storage::disk('local')->get($copyFile->storage_key))->toBe(Storage::disk('local')->get($publishedFile->storage_key));
tests/Feature/Admin/ContentManagementTest.php:240:    Storage::disk('local')->put($file->storage_key, 'Original theme file');
tests/Feature/Admin/ContentManagementTest.php:252:    expect(Storage::disk('local')->get($file->storage_key))->toBe('
Edited hero file
')
tests/Feature/Admin/DiscountManagementTest.php:239:test('admin discount form creates drafts by default and preserves expired discounts', function (): void {
tests/Feature/Catalog/CatalogFoundationTest.php:23:        ->and(Schema::hasColumns('inventory_items', ['store_id', 'variant_id', 'quantity_on_hand', 'quantity_reserved', 'policy']))->toBeTrue()
tests/Feature/Catalog/CatalogFoundationTest.php:99:        'quantity_reserved' => 0,
tests/Feature/Catalog/InventoryServiceTest.php:12:test('inventory reserve release commit and restock update quantities transactionally', function () {
tests/Feature/Catalog/InventoryServiceTest.php:19:    $item->forceFill(['quantity_on_hand' => 10, 'quantity_reserved' => 0, 'policy' => 'deny'])->save();
tests/Feature/Catalog/InventoryServiceTest.php:23:    $service->reserve($item, 4);
tests/Feature/Catalog/InventoryServiceTest.php:24:    expect($item->refresh()->quantity_reserved)->toBe(4);
tests/Feature/Catalog/InventoryServiceTest.php:27:    expect($item->refresh()->quantity_reserved)->toBe(3);
tests/Feature/Catalog/InventoryServiceTest.php:31:        ->and($item->quantity_reserved)->toBe(1);
tests/Feature/Catalog/InventoryServiceTest.php:44:    $denyItem->forceFill(['quantity_on_hand' => 1, 'quantity_reserved' => 0, 'policy' => 'deny'])->save();
tests/Feature/Catalog/InventoryServiceTest.php:46:    expect(fn () => app(InventoryService::class)->reserve($denyItem, 2))
tests/Feature/Catalog/InventoryServiceTest.php:52:    $continueItem->forceFill(['quantity_on_hand' => 0, 'quantity_reserved' => 0, 'policy' => 'continue'])->save();
tests/Feature/Catalog/InventoryServiceTest.php:54:    app(InventoryService::class)->reserve($continueItem, 2);
tests/Feature/Catalog/InventoryServiceTest.php:56:    expect($continueItem->refresh()->quantity_reserved)->toBe(2);
tests/Browser/Storefront/InventoryTest.php:102:            'quantity_reserved' => 0,
app/Services/CheckoutService.php:153:                    $this->inventory->reserve($inventory, $line->quantity);
app/Services/ThemeArchiveInstaller.php:70:                Storage::disk('local')->put($storageKey, $contents);
app/Models/Store.php:27:        'default_locale',
app/Services/OrderService.php:113:                    $this->commitReservedInventory($checkout);
app/Services/OrderService.php:158:            $this->commitReservedOrderInventory($order);
app/Services/OrderService.php:181:            $this->releaseReservedOrderInventory($order);
app/Services/OrderService.php:355:    private function commitReservedInventory(Checkout $checkout): void
app/Services/OrderService.php:362:    private function commitReservedOrderInventory(Order $order): void
app/Services/OrderService.php:371:    private function releaseReservedOrderInventory(Order $order): void
app/Services/InventoryService.php:24:    public function reserve(InventoryItem $item, int $quantity): void
app/Services/InventoryService.php:36:                'quantity_reserved' => $locked->quantity_reserved + $quantity,
app/Services/InventoryService.php:48:            if ($locked->quantity_reserved < $quantity) {
app/Services/InventoryService.php:49:                throw new InvalidArgumentException('Cannot release more inventory than is reserved.');
app/Services/InventoryService.php:53:                'quantity_reserved' => $locked->quantity_reserved - $quantity,
app/Services/InventoryService.php:65:            if ($locked->quantity_reserved < $quantity) {
app/Services/InventoryService.php:66:                throw new InvalidArgumentException('Cannot commit more inventory than is reserved.');
app/Services/InventoryService.php:71:                'quantity_reserved' => $locked->quantity_reserved - $quantity,
app/Services/InventoryService.php:91:        return $item->quantity_on_hand - $item->quantity_reserved;
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:31:            'default_locale' => ['sometimes', 'required', Rule::in(array_keys($this->localeOptions()))],
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:69:            'default_locale',
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:136:    private function localeOptions(): array
app/Providers/AppServiceProvider.php:21:use App\Observers\ProductObserver;
app/Providers/AppServiceProvider.php:117:        Product::observe(ProductObserver::class);
app/Models/InventoryItem.php:26:        'quantity_reserved',
app/Models/InventoryItem.php:35:        'quantity_reserved' => 0,
app/Models/InventoryItem.php:95:        return $this->quantity_on_hand - $this->quantity_reserved;
app/Models/InventoryItem.php:105:            'quantity_reserved' => 'integer',
app/Models/ProductVariant.php:90:                'quantity_reserved' => 0,
app/Http/Resources/Admin/V1/StoreSettingsResource.php:24:            'default_locale' => $this->default_locale,
app/Http/Resources/Admin/V1/ProductResource.php:69:                        'quantity_reserved' => $variant->inventoryItem?->quantity_reserved ?? 0,
app/Http/Resources/Admin/V1/DataExportResource.php:42:        if (! Storage::disk('local')->exists($this->storage_key)) {
app/Http/Resources/Admin/V1/DataExportResource.php:46:        return 'data:text/csv;charset=utf-8,'.rawurlencode(Storage::disk('local')->get($this->storage_key));
app/Livewire/Storefront/Collections/Show.php:78:                        ->orWhereColumn('quantity_on_hand', '>', 'quantity_reserved');
app/Livewire/Admin/Themes/Editor.php:65:        $this->fileContents = Storage::disk('local')->exists($file->storage_key)
app/Livewire/Admin/Themes/Editor.php:66:            ? Storage::disk('local')->get($file->storage_key)
app/Livewire/Admin/Themes/Editor.php:81:        Storage::disk('local')->put($file->storage_key, $this->fileContents);
app/Livewire/Storefront/Checkout/Show.php:203:            $this->step = 'reserved';
app/Livewire/Storefront/Checkout/Show.php:422:            CheckoutStatus::PaymentSelected => 'reserved',
app/Livewire/Admin/Themes/Index.php:81:                    $contents = Storage::disk('local')->exists($file->storage_key)
app/Livewire/Admin/Themes/Index.php:82:                        ? Storage::disk('local')->get($file->storage_key)
app/Livewire/Admin/Themes/Index.php:86:                    Storage::disk('local')->put($storageKey, $contents);
app/Livewire/Admin/Products/Form.php:504:                    'quantity_reserved' => 0,
app/Livewire/Admin/Navigation/Index.php:297:            ->groupBy(fn (array $item): string => $item['parent_key'] ?? 'root', preserveKeys: true);
app/Livewire/Admin/Settings/Index.php:54:        $this->defaultLocale = $store->default_locale;
app/Livewire/Admin/Settings/Index.php:70:            'defaultLocale' => ['required', Rule::in(array_keys($this->localeOptions()))],
app/Livewire/Admin/Settings/Index.php:80:            'default_locale' => $validated['defaultLocale'],
app/Livewire/Admin/Settings/Index.php:191:    public function localeOptions(): array
app/Livewire/Admin/Settings/Index.php:224:            'localeOptions' => $this->localeOptions(),
app/Livewire/Admin/Inventory/Index.php:42:            ->when($this->stockFilter === 'low', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) BETWEEN 1 AND 10'))
app/Livewire/Admin/Inventory/Index.php:43:            ->when($this->stockFilter === 'out', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) <= 0'))


sed -n '1,180p' app/Observers/ProductObserver.php


sed -n '1,180p' app/Observers/ProductObserver.php

<?php

namespace App\Observers;

use App\Models\Product;
use App\Services\SearchService;

class ProductObserver
{
    public function created(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function updated(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function deleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }

    public function forceDeleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }
}


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

namespace App\Observers;

use App\Models\Product;
use App\Services\SearchService;

class ProductObserver
{
    public function created(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function updated(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function deleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }

    public function forceDeleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }
}


The existing admin APIs use controller-local validation for simple resources, but order write routes already use FormRequest classes. Product writes are nested enough that I’m going to add dedicated requests, keep store membership in the existing middleware/controller pattern, and reuse the current product service where it fits.


php artisan make:request Api/Admin/V1/StoreProductRequest --no-interaction


php artisan make:request Api/Admin/V1/UpdateProductRequest --no-interaction


php artisan make:request Api/Admin/V1/CreateProductMediaUploadRequest --no-interaction


Output:
Chunk ID: 78423c
Wall time: 0.1538 seconds
Process exited with code 0
Original token count: 25
Output:

   INFO  Request [app/Http/Requests/Api/Admin/V1/StoreProductRequest.php] created successfully.


Output:
Chunk ID: 9cac50
Wall time: 0.1510 seconds
Process exited with code 0
Original token count: 26
Output:

   INFO  Request [app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php] created successfully.


Output:
Chunk ID: f3b7a9
Wall time: 0.1535 seconds
Process exited with code 0
Original token count: 28
Output:

   INFO  Request [app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php] created successfully.


php artisan make:request Api/Admin/V1/StoreProductRequest --no-interaction

INFO  Request [app/Http/Requests/Api/Admin/V1/StoreProductRequest.php] created successfully.


php artisan make:request Api/Admin/V1/UpdateProductRequest --no-interaction

INFO  Request [app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php] created successfully.


php artisan make:request Api/Admin/V1/CreateProductMediaUploadRequest --no-interaction

INFO  Request [app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php] created successfully.


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/StoreProductRequest.php


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/StoreProductRequest.php

<?php

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

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest 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 [
            //
        ];
    }
}


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

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

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest 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 [
            //
        ];
    }
}


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php

<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateProductMediaUploadRequest 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 [
            //
        ];
    }
}


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php

<?php

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

use Illuminate\Foundation\Http\FormRequest;

class UpdateProductRequest 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 [
            //
        ];
    }
}


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

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

use Illuminate\Foundation\Http\FormRequest;

class UpdateProductRequest 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 [
            //
        ];
    }
}


Output:
Chunk ID: 0152e5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 146
Output:
<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateProductMediaUploadRequest 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 artisan tinker --execute 'var_export(Illuminate\Support\Facades\Storage::disk("local")->temporaryUploadUrl("test.txt", now()->addMinutes(5)));'


Output:
Chunk ID: 3e19d9
Wall time: 0.2194 seconds
Process exited with code 0
Original token count: 21
Output:

   RuntimeException  This driver does not support creating temporary upload URLs.


php artisan tinker --execute 'var_export(Illuminate\Support\Facades\Storage::disk("local")->temporaryUploadUrl("test.txt", now()->addMinutes(5)));'

RuntimeException  This driver does not support creating temporary upload URLs.


php artisan tinker --execute 'var_export(Illuminate\Support\Facades\Storage::disk("public")->temporaryUploadUrl("test.txt", now()->addMinutes(5)));'


Output:
Chunk ID: 612efa
Wall time: 0.0940 seconds
Process exited with code 0
Original token count: 21
Output:

   RuntimeException  This driver does not support creating temporary upload URLs.


sed -n '1,140p' database/migrations/2026_05_03_213903_create_collection_products_table.php


sed -n '1,140p' database/migrations/2026_05_03_213903_create_collection_products_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('collection_products', function (Blueprint $table) {
            $table->foreignId('collection_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->unsignedInteger('position')->default(0);

            $table->primary(['collection_id', 'product_id']);
            $table->index('product_id');
            $table->index(['collection_id', 'position']);
        });
    }

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


Output:
Chunk ID: 0076e5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 220
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('collection_products', function (Blueprint $table) {
            $table->foreignId('collection_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->unsignedInteger('position')->default(0);

            $table->primary(['collection_id', 'product_id']);
            $table->index('product_id');
            $table->index(['collection_id', 'position']);
        });
    }

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


sed -n '1,160p' database/factories/CollectionFactory.php


sed -n '1,160p' database/factories/CollectionFactory.php

<?php

namespace Database\Factories;

use App\Enums\CollectionStatus;
use App\Enums\CollectionType;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

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

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'description_html' => '<p>'.fake()->sentence().'</p>',
            'type' => CollectionType::Manual,
            'status' => CollectionStatus::Active,
        ];
    }

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

    public function automated(): static
    {
        return $this->state(fn (array $attributes): array => [
            'type' => CollectionType::Automated,
        ]);
    }
}


Output:
Chunk ID: 302d00
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 301
Output:
<?php

namespace Database\Factories;

use App\Enums\CollectionStatus;
use App\Enums\CollectionType;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

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

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'description_html' => '<p>'.fake()->sentence().'</p>',
            'type' => CollectionType::Manual,
            'status' => CollectionStatus::Active,
        ];
    }

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

    public function automated(): static
    {
        return $this->state(fn (array $attributes): array => [
            'type' => CollectionType::Automated,
        ]);
    }
}


I’ve got the shape now: this slice needs product create/update/archive plus media presign, all behind `write-products`. I’m editing the request validation, routes, controller logic, and focused API tests in one pass.


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StoreProductRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\ProductVariant;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Validator;

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

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
+        $store = $this->routeStore();
+
         return [
-            //
+            'title' => ['required', 'string', 'max:255'],
+            'handle' => [
+                'sometimes',
+                'nullable',
+                'string',
+                'max:255',
+                'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
+                Rule::unique('products', 'handle')->where('store_id', $store->getKey()),
+            ],
+            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
+            'vendor' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'product_type' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'status' => ['sometimes', Rule::in(['draft', 'active'])],
+            'tags' => ['sometimes', 'array', 'max:50'],
+            'tags.*' => ['string', 'max:255'],
+            'options' => ['sometimes', 'array', 'max:3'],
+            'options.*.name' => ['required_with:options', 'string', 'max:255'],
+            'options.*.position' => ['required_with:options', 'integer', 'min:1', 'max:3'],
+            'options.*.values' => ['sometimes', 'array', 'max:100'],
+            'options.*.values.*' => ['string', 'max:255'],
+            'variants' => ['required', 'array', 'min:1', 'max:100'],
+            'variants.*.sku' => ['required', 'string', 'max:255'],
+            'variants.*.barcode' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'variants.*.price_amount' => ['required', 'integer', 'min:0'],
+            'variants.*.compare_at_amount' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.currency' => ['sometimes', 'string', 'size:3'],
+            'variants.*.weight_g' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.requires_shipping' => ['sometimes', 'boolean'],
+            'variants.*.is_default' => ['required', 'boolean'],
+            'variants.*.position' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.status' => ['sometimes', Rule::in(['active', 'archived'])],
+            'variants.*.option_values' => ['sometimes', 'array'],
+            'variants.*.option_values.*.option_name' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.option_values.*.value' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.inventory' => ['sometimes', 'array'],
+            'variants.*.inventory.quantity_on_hand' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.inventory.policy' => ['sometimes', Rule::in(['deny', 'continue'])],
+            'collections' => ['sometimes', 'array'],
+            'collections.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
         ];
     }
+
+    /**
+     * @return array<int, callable>
+     */
+    public function after(): array
+    {
+        return [
+            function (Validator $validator): void {
+                $this->validateVariantDefaults($validator);
+                $this->validateVariantPrices($validator);
+                $this->validateVariantSkus($validator);
+            },
+        ];
+    }
+
+    private function routeStore(): Store
+    {
+        $store = $this->route('store');
+
+        return $store instanceof Store ? $store : Store::query()->findOrFail($store);
+    }
+
+    private function validateVariantDefaults(Validator $validator): void
+    {
+        $variants = $this->input('variants', []);
+
+        if (! is_array($variants)) {
+            return;
+        }
+
+        $defaultCount = collect($variants)
+            ->filter(fn (mixed $variant): bool => is_array($variant) && (bool) ($variant['is_default'] ?? false))
+            ->count();
+
+        if ($defaultCount !== 1) {
+            $validator->errors()->add('variants', __('Exactly one product variant must be marked as default.'));
+        }
+    }
+
+    private function validateVariantPrices(Validator $validator): void
+    {
+        foreach ($this->input('variants', []) as $index => $variant) {
+            if (! is_array($variant) || ! isset($variant['compare_at_amount'])) {
+                continue;
+            }
+
+            $compareAtAmount = $variant['compare_at_amount'];
+
+            if ($compareAtAmount === null || $compareAtAmount === '') {
+                continue;
+            }
+
+            if ((int) $compareAtAmount <= (int) ($variant['price_amount'] ?? 0)) {
+                $validator->errors()->add("variants.{$index}.compare_at_amount", __('The compare at amount must be greater than the price amount.'));
+            }
+        }
+    }
+
+    private function validateVariantSkus(Validator $validator): void
+    {
+        $skus = collect($this->input('variants', []))
+            ->map(fn (mixed $variant): string => is_array($variant) ? trim((string) ($variant['sku'] ?? '')) : '')
+            ->filter()
+            ->values();
+
+        if ($skus->duplicates()->isNotEmpty()) {
+            $validator->errors()->add('variants.0.sku', __('Each variant SKU must be unique for this store.'));
+
+            return;
+        }
+
+        $conflictingSku = ProductVariant::withoutGlobalScopes()
+            ->whereIn('sku', $skus->all())
+            ->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->routeStore()->getKey());
+            })
+            ->value('sku');
+
+        if ($conflictingSku !== null) {
+            $validator->errors()->add('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));
+        }
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/StoreProductRequest.php

@@ -4,3 +4,8 @@

+use App\Models\ProductVariant;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Validator;

@@ -8,8 +13,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +19,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -23,6 +23,130 @@
     {
+        $store = $this->routeStore();
+
         return [
-            //
+            'title' => ['required', 'string', 'max:255'],
+            'handle' => [
+                'sometimes',
+                'nullable',
+                'string',
+                'max:255',
+                'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
+                Rule::unique('products', 'handle')->where('store_id', $store->getKey()),
+            ],
+            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
+            'vendor' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'product_type' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'status' => ['sometimes', Rule::in(['draft', 'active'])],
+            'tags' => ['sometimes', 'array', 'max:50'],
+            'tags.*' => ['string', 'max:255'],
+            'options' => ['sometimes', 'array', 'max:3'],
+            'options.*.name' => ['required_with:options', 'string', 'max:255'],
+            'options.*.position' => ['required_with:options', 'integer', 'min:1', 'max:3'],
+            'options.*.values' => ['sometimes', 'array', 'max:100'],
+            'options.*.values.*' => ['string', 'max:255'],
+            'variants' => ['required', 'array', 'min:1', 'max:100'],
+            'variants.*.sku' => ['required', 'string', 'max:255'],
+            'variants.*.barcode' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'variants.*.price_amount' => ['required', 'integer', 'min:0'],
+            'variants.*.compare_at_amount' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.currency' => ['sometimes', 'string', 'size:3'],
+            'variants.*.weight_g' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.requires_shipping' => ['sometimes', 'boolean'],
+            'variants.*.is_default' => ['required', 'boolean'],
+            'variants.*.position' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.status' => ['sometimes', Rule::in(['active', 'archived'])],
+            'variants.*.option_values' => ['sometimes', 'array'],
+            'variants.*.option_values.*.option_name' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.option_values.*.value' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.inventory' => ['sometimes', 'array'],
+            'variants.*.inventory.quantity_on_hand' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.inventory.policy' => ['sometimes', Rule::in(['deny', 'continue'])],
+            'collections' => ['sometimes', 'array'],
+            'collections.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
         ];
     }
+
+    /**
+     * @return array<int, callable>
+     */
+    public function after(): array
+    {
+        return [
+            function (Validator $validator): void {
+                $this->validateVariantDefaults($validator);
+                $this->validateVariantPrices($validator);
+                $this->validateVariantSkus($validator);
+            },
+        ];
+    }
+
+    private function routeStore(): Store
+    {
+        $store = $this->route('store');
+
+        return $store instanceof Store ? $store : Store::query()->findOrFail($store);
+    }
+
+    private function validateVariantDefaults(Validator $validator): void
+    {
+        $variants = $this->input('variants', []);
+
+        if (! is_array($variants)) {
+            return;
+        }
+
+        $defaultCount = collect($variants)
+            ->filter(fn (mixed $variant): bool => is_array($variant) && (bool) ($variant['is_default'] ?? false))
+            ->count();
+
+        if ($defaultCount !== 1) {
+            $validator->errors()->add('variants', __('Exactly one product variant must be marked as default.'));
+        }
+    }
+
+    private function validateVariantPrices(Validator $validator): void
+    {
+        foreach ($this->input('variants', []) as $index => $variant) {
+            if (! is_array($variant) || ! isset($variant['compare_at_amount'])) {
+                continue;
+            }
+
+            $compareAtAmount = $variant['compare_at_amount'];
+
+            if ($compareAtAmount === null || $compareAtAmount === '') {
+                continue;
+            }
+
+            if ((int) $compareAtAmount <= (int) ($variant['price_amount'] ?? 0)) {
+                $validator->errors()->add("variants.{$index}.compare_at_amount", __('The compare at amount must be greater than the price amount.'));
+            }
+        }
+    }
+
+    private function validateVariantSkus(Validator $validator): void
+    {
+        $skus = collect($this->input('variants', []))
+            ->map(fn (mixed $variant): string => is_array($variant) ? trim((string) ($variant['sku'] ?? '')) : '')
+            ->filter()
+            ->values();
+
+        if ($skus->duplicates()->isNotEmpty()) {
+            $validator->errors()->add('variants.0.sku', __('Each variant SKU must be unique for this store.'));
+
+            return;
+        }
+
+        $conflictingSku = ProductVariant::withoutGlobalScopes()
+            ->whereIn('sku', $skus->all())
+            ->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->routeStore()->getKey());
+            })
+            ->value('sku');
+
+        if ($conflictingSku !== null) {
+            $validator->errors()->add('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));
+        }
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StoreProductRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Validator;

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

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
+        $store = $this->routeStore();
+        $product = $this->routeProduct();
+
         return [
-            //
+            'title' => ['sometimes', 'string', 'max:255'],
+            'handle' => [
+                'sometimes',
+                'nullable',
+                'string',
+                'max:255',
+                'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
+                Rule::unique('products', 'handle')
+                    ->where('store_id', $store->getKey())
+                    ->ignore($product?->getKey()),
+            ],
+            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
+            'vendor' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'product_type' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'status' => ['sometimes', Rule::in(['draft', 'active', 'archived'])],
+            'tags' => ['sometimes', 'array', 'max:50'],
+            'tags.*' => ['string', 'max:255'],
+            'options' => ['sometimes', 'array', 'max:3'],
+            'options.*.name' => ['required_with:options', 'string', 'max:255'],
+            'options.*.position' => ['required_with:options', 'integer', 'min:1', 'max:3'],
+            'options.*.values' => ['sometimes', 'array', 'max:100'],
+            'options.*.values.*' => ['string', 'max:255'],
+            'variants' => ['sometimes', 'array', 'min:1', 'max:100'],
+            'variants.*.id' => ['sometimes', 'integer'],
+            'variants.*.sku' => ['required_with:variants', 'string', 'max:255'],
+            'variants.*.barcode' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'variants.*.price_amount' => ['required_with:variants', 'integer', 'min:0'],
+            'variants.*.compare_at_amount' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.currency' => ['sometimes', 'string', 'size:3'],
+            'variants.*.weight_g' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.requires_shipping' => ['sometimes', 'boolean'],
+            'variants.*.is_default' => ['required_with:variants', 'boolean'],
+            'variants.*.position' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.status' => ['sometimes', Rule::in(['active', 'archived'])],
+            'variants.*.option_values' => ['sometimes', 'array'],
+            'variants.*.option_values.*.option_name' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.option_values.*.value' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.inventory' => ['sometimes', 'array'],
+            'variants.*.inventory.quantity_on_hand' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.inventory.policy' => ['sometimes', Rule::in(['deny', 'continue'])],
+            'collections' => ['sometimes', 'array'],
+            'collections.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
         ];
     }
+
+    /**
+     * @return array<int, callable>
+     */
+    public function after(): array
+    {
+        return [
+            function (Validator $validator): void {
+                $this->validateVariantDefaults($validator);
+                $this->validateVariantPrices($validator);
+                $this->validateVariantSkus($validator);
+                $this->validateVariantIds($validator);
+            },
+        ];
+    }
+
+    private function routeStore(): Store
+    {
+        $store = $this->route('store');
+
+        return $store instanceof Store ? $store : Store::query()->findOrFail($store);
+    }
+
+    private function routeProduct(): ?Product
+    {
+        $product = $this->route('product');
+
+        return $product instanceof Product ? $product : null;
+    }
+
+    private function validateVariantDefaults(Validator $validator): void
+    {
+        if (! $this->has('variants')) {
+            return;
+        }
+
+        $variants = $this->input('variants', []);
+
+        if (! is_array($variants)) {
+            return;
+        }
+
+        $defaultCount = collect($variants)
+            ->filter(fn (mixed $variant): bool => is_array($variant) && (bool) ($variant['is_default'] ?? false))
+            ->count();
+
+        if ($defaultCount !== 1) {
+            $validator->errors()->add('variants', __('Exactly one product variant must be marked as default.'));
+        }
+    }
+
+    private function validateVariantPrices(Validator $validator): void
+    {
+        foreach ($this->input('variants', []) as $index => $variant) {
+            if (! is_array($variant) || ! isset($variant['compare_at_amount'])) {
+                continue;
+            }
+
+            $compareAtAmount = $variant['compare_at_amount'];
+
+            if ($compareAtAmount === null || $compareAtAmount === '') {
+                continue;
+            }
+
+            if ((int) $compareAtAmount <= (int) ($variant['price_amount'] ?? 0)) {
+                $validator->errors()->add("variants.{$index}.compare_at_amount", __('The compare at amount must be greater than the price amount.'));
+            }
+        }
+    }
+
+    private function validateVariantSkus(Validator $validator): void
+    {
+        if (! $this->has('variants')) {
+            return;
+        }
+
+        $skus = collect($this->input('variants', []))
+            ->map(fn (mixed $variant): string => is_array($variant) ? trim((string) ($variant['sku'] ?? '')) : '')
+            ->filter()
+            ->values();
+
+        if ($skus->duplicates()->isNotEmpty()) {
+            $validator->errors()->add('variants.0.sku', __('Each variant SKU must be unique for this store.'));
+
+            return;
+        }
+
+        $product = $this->routeProduct();
+
+        $conflictingSku = ProductVariant::withoutGlobalScopes()
+            ->whereIn('sku', $skus->all())
+            ->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->routeStore()->getKey());
+            })
+            ->when($product instanceof Product, function (Builder $query) use ($product): void {
+                $query->where('product_id', '!=', $product->getKey());
+            })
+            ->value('sku');
+
+        if ($conflictingSku !== null) {
+            $validator->errors()->add('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));
+        }
+    }
+
+    private function validateVariantIds(Validator $validator): void
+    {
+        $product = $this->routeProduct();
+
+        if (! $product instanceof Product) {
+            return;
+        }
+
+        $variantIds = collect($this->input('variants', []))
+            ->pluck('id')
+            ->filter()
+            ->map(fn (mixed $variantId): int => (int) $variantId)
+            ->values();
+
+        if ($variantIds->isEmpty()) {
+            return;
+        }
+
+        $validCount = ProductVariant::withoutGlobalScopes()
+            ->where('product_id', $product->getKey())
+            ->whereIn('id', $variantIds->all())
+            ->count();
+
+        if ($validCount !== $variantIds->count()) {
+            $validator->errors()->add('variants', __('Variant IDs must belong to the product being updated.'));
+        }
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php

@@ -4,3 +4,9 @@

+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Validator;

@@ -8,8 +14,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +20,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -23,6 +24,183 @@
     {
+        $store = $this->routeStore();
+        $product = $this->routeProduct();
+
+        return [
+            'title' => ['sometimes', 'string', 'max:255'],
+            'handle' => [
+                'sometimes',
+                'nullable',
+                'string',
+                'max:255',
+                'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
+                Rule::unique('products', 'handle')
+                    ->where('store_id', $store->getKey())
+                    ->ignore($product?->getKey()),
+            ],
+            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
+            'vendor' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'product_type' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'status' => ['sometimes', Rule::in(['draft', 'active', 'archived'])],
+            'tags' => ['sometimes', 'array', 'max:50'],
+            'tags.*' => ['string', 'max:255'],
+            'options' => ['sometimes', 'array', 'max:3'],
+            'options.*.name' => ['required_with:options', 'string', 'max:255'],
+            'options.*.position' => ['required_with:options', 'integer', 'min:1', 'max:3'],
+            'options.*.values' => ['sometimes', 'array', 'max:100'],
+            'options.*.values.*' => ['string', 'max:255'],
+            'variants' => ['sometimes', 'array', 'min:1', 'max:100'],
+            'variants.*.id' => ['sometimes', 'integer'],
+            'variants.*.sku' => ['required_with:variants', 'string', 'max:255'],
+            'variants.*.barcode' => ['sometimes', 'nullable', 'string', 'max:255'],
+            'variants.*.price_amount' => ['required_with:variants', 'integer', 'min:0'],
+            'variants.*.compare_at_amount' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.currency' => ['sometimes', 'string', 'size:3'],
+            'variants.*.weight_g' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            'variants.*.requires_shipping' => ['sometimes', 'boolean'],
+            'variants.*.is_default' => ['required_with:variants', 'boolean'],
+            'variants.*.position' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.status' => ['sometimes', Rule::in(['active', 'archived'])],
+            'variants.*.option_values' => ['sometimes', 'array'],
+            'variants.*.option_values.*.option_name' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.option_values.*.value' => ['required_with:variants.*.option_values', 'string', 'max:255'],
+            'variants.*.inventory' => ['sometimes', 'array'],
+            'variants.*.inventory.quantity_on_hand' => ['sometimes', 'integer', 'min:0'],
+            'variants.*.inventory.policy' => ['sometimes', Rule::in(['deny', 'continue'])],
+            'collections' => ['sometimes', 'array'],
+            'collections.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
+        ];
+    }
+
+    /**
+     * @return array<int, callable>
+     */
+    public function after(): array
+    {
         return [
-            //
+            function (Validator $validator): void {
+                $this->validateVariantDefaults($validator);
+                $this->validateVariantPrices($validator);
+                $this->validateVariantSkus($validator);
+                $this->validateVariantIds($validator);
+            },
         ];
     }
+
+    private function routeStore(): Store
+    {
+        $store = $this->route('store');
+
+        return $store instanceof Store ? $store : Store::query()->findOrFail($store);
+    }
+
+    private function routeProduct(): ?Product
+    {
+        $product = $this->route('product');
+
+        return $product instanceof Product ? $product : null;
+    }
+
+    private function validateVariantDefaults(Validator $validator): void
+    {
+        if (! $this->has('variants')) {
+            return;
+        }
+
+        $variants = $this->input('variants', []);
+
+        if (! is_array($variants)) {
+            return;
+        }
+
+        $defaultCount = collect($variants)
+            ->filter(fn (mixed $variant): bool => is_array($variant) && (bool) ($variant['is_default'] ?? false))
+            ->count();
+
+        if ($defaultCount !== 1) {
+            $validator->errors()->add('variants', __('Exactly one product variant must be marked as default.'));
+        }
+    }
+
+    private function validateVariantPrices(Validator $validator): void
+    {
+        foreach ($this->input('variants', []) as $index => $variant) {
+            if (! is_array($variant) || ! isset($variant['compare_at_amount'])) {
+                continue;
+            }
+
+            $compareAtAmount = $variant['compare_at_amount'];
+
+            if ($compareAtAmount === null || $compareAtAmount === '') {
+                continue;
+            }
+
+            if ((int) $compareAtAmount <= (int) ($variant['price_amount'] ?? 0)) {
+                $validator->errors()->add("variants.{$index}.compare_at_amount", __('The compare at amount must be greater than the price amount.'));
+            }
+        }
+    }
+
+    private function validateVariantSkus(Validator $validator): void
+    {
+        if (! $this->has('variants')) {
+            return;
+        }
+
+        $skus = collect($this->input('variants', []))
+            ->map(fn (mixed $variant): string => is_array($variant) ? trim((string) ($variant['sku'] ?? '')) : '')
+            ->filter()
+            ->values();
+
+        if ($skus->duplicates()->isNotEmpty()) {
+            $validator->errors()->add('variants.0.sku', __('Each variant SKU must be unique for this store.'));
+
+            return;
+        }
+
+        $product = $this->routeProduct();
+
+        $conflictingSku = ProductVariant::withoutGlobalScopes()
+            ->whereIn('sku', $skus->all())
+            ->whereHas('product', function (Builder $query): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $this->routeStore()->getKey());
+            })
+            ->when($product instanceof Product, function (Builder $query) use ($product): void {
+                $query->where('product_id', '!=', $product->getKey());
+            })
+            ->value('sku');
+
+        if ($conflictingSku !== null) {
+            $validator->errors()->add('variants.0.sku', __('The SKU [:sku] is already used in this store.', ['sku' => $conflictingSku]));
+        }
+    }
+
+    private function validateVariantIds(Validator $validator): void
+    {
+        $product = $this->routeProduct();
+
+        if (! $product instanceof Product) {
+            return;
+        }
+
+        $variantIds = collect($this->input('variants', []))
+            ->pluck('id')
+            ->filter()
+            ->map(fn (mixed $variantId): int => (int) $variantId)
+            ->values();
+
+        if ($variantIds->isEmpty()) {
+            return;
+        }
+
+        $validCount = ProductVariant::withoutGlobalScopes()
+            ->where('product_id', $product->getKey())
+            ->whereIn('id', $variantIds->all())
+            ->count();
+
+        if ($validCount !== $variantIds->count()) {
+            $validator->errors()->add('variants', __('Variant IDs must belong to the product being updated.'));
+        }
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Validator;

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

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'filename' => ['required', 'string', 'max:255'],
+            'content_type' => ['required', Rule::in(['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'video/mp4'])],
+            'byte_size' => ['required', 'integer', 'min:1'],
         ];
     }
+
+    /**
+     * @return array<int, callable>
+     */
+    public function after(): array
+    {
+        return [
+            function (Validator $validator): void {
+                $this->validateExtension($validator);
+                $this->validateByteSize($validator);
+            },
+        ];
+    }
+
+    private function validateExtension(Validator $validator): void
+    {
+        $extension = strtolower(pathinfo((string) $this->input('filename'), PATHINFO_EXTENSION));
+        $allowedExtensions = match ((string) $this->input('content_type')) {
+            'image/jpeg' => ['jpg', 'jpeg'],
+            'image/png' => ['png'],
+            'image/webp' => ['webp'],
+            'image/avif' => ['avif'],
+            'video/mp4' => ['mp4'],
+            default => [],
+        };
+
+        if ($extension === '' || ! in_array($extension, $allowedExtensions, true)) {
+            $validator->errors()->add('filename', __('The filename extension must match the content type.'));
+        }
+    }
+
+    private function validateByteSize(Validator $validator): void
+    {
+        $limit = $this->input('content_type') === 'video/mp4'
+            ? 500 * 1024 * 1024
+            : 50 * 1024 * 1024;
+
+        if ((int) $this->input('byte_size') > $limit) {
+            $validator->errors()->add('byte_size', __('The uploaded media file is too large.'));
+        }
+    }
 }
*** End Patch