stdout:
Success. Updated the following files:
M app/Http/Controllers/Api/Storefront/V1/CartController.php
M app/Http/Controllers/Api/Storefront/V1/CartLineController.php
M app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
M routes/api.php
M app/Providers/AppServiceProvider.php

unified_diff:
@@ -5,2 +5,3 @@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
 use App\Http\Resources\Storefront\V1\CartResource;
@@ -13,4 +14,6 @@
 {
-    public function store(CartService $carts): JsonResponse
+    public function store(StoreCartRequest $request, CartService $carts): JsonResponse
     {
+        $request->validated();
+
         return CartResource::make($this->loadCart($carts->create($this->currentStore())))

unified_diff:
@@ -31,4 +31,10 @@
         try {
+            $checkout = $checkouts->createFromCart($cart);
+            $checkout->forceFill([
+                'email' => (string) $request->validated('email'),
+            ])->save();
+            app(PricingEngine::class)->calculate($checkout);
+
             return CheckoutResource::make($this->loadCheckout(
-                $checkouts->createFromCart($cart),
+                $checkout->refresh(),
             ))->response()->setStatusCode(201);
@@ -46,4 +52,7 @@
     {
+        $addressData = $request->validated();
+        $addressData['email'] ??= $checkout->email;
+
         try {
-            return CheckoutResource::make($this->loadCheckout($checkouts->setAddress($checkout, $request->validated())));
+            return CheckoutResource::make($this->loadCheckout($checkouts->setAddress($checkout, $addressData)));
         } catch (InvalidCheckoutTransitionException $exception) {
@@ -86,2 +95,12 @@

+    public function destroyDiscount(Checkout $checkout, PricingEngine $pricing): CheckoutResource
+    {
+        abort_if($checkout->discount_code === null, 404);
+
+        $checkout->forceFill(['discount_code' => null])->save();
+        $pricing->calculate($checkout);
+
+        return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
+    }
+
     public function paymentMethod(SelectCheckoutPaymentRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse

unified_diff:
@@ -71,3 +71,5 @@
         RateLimiter::for('checkout', function (Request $request): Limit {
-            return Limit::perMinute(10)->by($request->session()->getId() ?: $request->ip());
+            $sessionId = $request->hasSession() ? $request->session()->getId() : null;
+
+            return Limit::perMinute(10)->by($sessionId ?: $request->ip());
         });

unified_diff:
@@ -8,2 +8,3 @@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Storefront\V1\DestroyCartLineRequest;
 use App\Http\Requests\Api\Storefront\V1\StoreCartLineRequest;
@@ -25,6 +26,8 @@
                 (int) $request->validated('quantity'),
-                $request->validated('expected_version'),
+                $request->expectedVersion(),
             );

-            return CartResource::make($this->loadCart($cart->refresh()));
+            return CartResource::make($this->loadCart($cart->refresh()))
+                ->response()
+                ->setStatusCode(201);
         } catch (CartVersionMismatchException $exception) {
@@ -45,3 +48,3 @@
                 (int) $request->validated('quantity'),
-                $request->validated('expected_version'),
+                $request->expectedVersion(),
             );
@@ -56,3 +59,3 @@

-    public function destroy(Cart $cart, CartLine $cartLine, CartService $carts): CartResource
+    public function destroy(DestroyCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
     {
@@ -60,3 +63,9 @@

-        $carts->removeLine($cart, $cartLine->getKey());
+        try {
+            $carts->removeLine($cart, $cartLine->getKey(), $request->expectedVersion());
+        } catch (CartVersionMismatchException $exception) {
+            return $this->versionMismatch($exception, $cart);
+        } catch (InvalidCartOperationException $exception) {
+            return response()->json(['message' => $exception->getMessage()], 422);
+        }

@@ -69,2 +78,4 @@
             'message' => $exception->getMessage(),
+            'expected_cart_version' => $exception->expectedVersion,
+            'current_cart_version' => $exception->currentVersion,
             'cart' => CartResource::make($this->loadCart($cart->refresh())),

unified_diff:
@@ -17,8 +17,11 @@

-        Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
-        Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
-        Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
-        Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
-        Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
-        Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
+        Route::middleware('throttle:checkout')->group(function (): void {
+            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
+            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
+            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
+            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
+            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
+            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
+            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
+        });
     });


output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Storefront/V1/CartController.php\nM app/Http/Controllers/Api/Storefront/V1/CartLineController.php\nM app/Http/Controllers/Api/Storefront/V1/CheckoutController.php\nM routes/api.php\nM app/Providers/AppServiceProvider.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: routes/api.php
@@
-Route::middleware(['store.resolve', 'throttle:api.storefront'])
+Route::middleware('store.resolve')
     ->prefix('storefront/v1')
     ->name('api.storefront.v1.')
     ->group(function (): void {
-        Route::post('carts', [CartController::class, 'store'])->name('carts.store');
-        Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
-        Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
-        Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
-        Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
+        Route::middleware('throttle:api.storefront')->group(function (): void {
+            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
+            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
+            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
+            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
+            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
+        });

         Route::middleware('throttle:checkout')->group(function (): void {
             Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
*** End Patch


stdout:
Success. Updated the following files:
M routes/api.php

type: update

unified_diff:
@@ -7,3 +7,3 @@

-Route::middleware(['store.resolve', 'throttle:api.storefront'])
+Route::middleware('store.resolve')
     ->prefix('storefront/v1')
@@ -11,7 +11,9 @@
     ->group(function (): void {
-        Route::post('carts', [CartController::class, 'store'])->name('carts.store');
-        Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
-        Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
-        Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
-        Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
+        Route::middleware('throttle:api.storefront')->group(function (): void {
+            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
+            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
+            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
+            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
+            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
+        });

status: completed


output: {"output":"Success. Updated the following files:\nM routes/api.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
@@
-    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts): CheckoutResource|JsonResponse
+    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts, PricingEngine $pricing): CheckoutResource|JsonResponse
@@
             $checkout->forceFill([
                 'email' => (string) $request->validated('email'),
             ])->save();
-            app(PricingEngine::class)->calculate($checkout);
+            $pricing->calculate($checkout);
*** End Patch


stdout:
Success. Updated the following files:
M app/Http/Controllers/Api/Storefront/V1/CheckoutController.php

unified_diff:
@@ -26,3 +26,3 @@
 {
-    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts): CheckoutResource|JsonResponse
+    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts, PricingEngine $pricing): CheckoutResource|JsonResponse
     {
@@ -35,3 +35,3 @@
             ])->save();
-            app(PricingEngine::class)->calculate($checkout);
+            $pricing->calculate($checkout);


output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Storefront/V1/CheckoutController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Delete File: tests/Feature/Api/StorefrontCartApiTest.php
*** Add File: tests/Feature/Api/StorefrontCartApiTest.php
+<?php
+
+use App\Models\Cart;
+use App\Models\InventoryItem;
+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\Store;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function storefrontApiCartStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function storefrontApiCartVariant(Store $store): ProductVariant
+{
+    $product = Product::withoutGlobalScopes()
+        ->where('store_id', $store->getKey())
+        ->where('handle', 'classic-cotton-t-shirt')
+        ->firstOrFail();
+
+    $variant = ProductVariant::withoutGlobalScopes()
+        ->where('product_id', $product->getKey())
+        ->oldest('position')
+        ->firstOrFail();
+
+    InventoryItem::withoutGlobalScopes()
+        ->where('variant_id', $variant->getKey())
+        ->update([
+            'quantity_on_hand' => 20,
+            'quantity_reserved' => 0,
+        ]);
+
+    return $variant->refresh();
+}
+
+test('storefront cart api creates and retrieves carts for the resolved store', function (): void {
+    $store = storefrontApiCartStore();
+
+    $response = $this->withHeader('Host', 'shop.test')
+        ->postJson('/api/storefront/v1/carts', ['currency' => $store->default_currency]);
+
+    $response
+        ->assertCreated()
+        ->assertJsonPath('data.store_id', $store->getKey())
+        ->assertJsonPath('data.currency', 'EUR')
+        ->assertJsonPath('data.status', 'active')
+        ->assertJsonPath('data.cart_version', 1)
+        ->assertJsonPath('data.line_count', 0)
+        ->assertJsonPath('data.totals.total', 0);
+
+    $this->withHeader('Host', 'shop.test')
+        ->getJson("/api/storefront/v1/carts/{$response['data']['id']}")
+        ->assertOk()
+        ->assertJsonPath('data.id', $response['data']['id'])
+        ->assertJsonPath('data.lines', []);
+});
+
+test('storefront cart api adds updates and removes line items with version increments', function (): void {
+    $store = storefrontApiCartStore();
+    $variant = storefrontApiCartVariant($store);
+
+    $cartId = $this->withHeader('Host', 'shop.test')
+        ->postJson('/api/storefront/v1/carts')
+        ->assertCreated()['data']['id'];
+
+    $addResponse = $this->withHeader('Host', 'shop.test')
+        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
+            'variant_id' => $variant->getKey(),
+            'quantity' => 2,
+        ]);
+
+    $addResponse
+        ->assertCreated()
+        ->assertJsonPath('data.cart_version', 2)
+        ->assertJsonPath('data.line_count', 2)
+        ->assertJsonPath('data.lines.0.quantity', 2)
+        ->assertJsonPath('data.lines.0.line_total_amount', 5000);
+
+    $lineId = $addResponse['data']['lines'][0]['id'];
+
+    $this->withHeader('Host', 'shop.test')
+        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
+            'quantity' => 3,
+            'cart_version' => 2,
+        ])
+        ->assertOk()
+        ->assertJsonPath('data.cart_version', 3)
+        ->assertJsonPath('data.lines.0.quantity', 3)
+        ->assertJsonPath('data.totals.total', 7500);
+
+    $this->withHeader('Host', 'shop.test')
+        ->deleteJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
+            'cart_version' => 3,
+        ])
+        ->assertOk()
+        ->assertJsonPath('data.cart_version', 4)
+        ->assertJsonPath('data.line_count', 0)
+        ->assertJsonPath('data.lines', []);
+
+    expect(Cart::withoutGlobalScopes()->findOrFail($cartId)->cart_version)->toBe(4);
+});
+
+test('storefront cart api returns the current cart on version conflicts', function (): void {
+    $store = storefrontApiCartStore();
+    $variant = storefrontApiCartVariant($store);
+
+    $cartId = $this->withHeader('Host', 'shop.test')
+        ->postJson('/api/storefront/v1/carts')
+        ->assertCreated()['data']['id'];
+
+    $lineId = $this->withHeader('Host', 'shop.test')
+        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
+            'variant_id' => $variant->getKey(),
+            'quantity' => 1,
+        ])
+        ->assertCreated()['data']['lines'][0]['id'];
+
+    $this->withHeader('Host', 'shop.test')
+        ->putJson("/api/storefront/v1/carts/{$cartId}/lines/{$lineId}", [
+            'quantity' => 2,
+            'cart_version' => 1,
+        ])
+        ->assertConflict()
+        ->assertJsonPath('expected_cart_version', 1)
+        ->assertJsonPath('current_cart_version', 2)
+        ->assertJsonPath('cart.cart_version', 2)
+        ->assertJsonPath('cart.lines.0.quantity', 1);
+});
+
+test('storefront cart api validates tenant and request boundaries', function (): void {
+    $store = storefrontApiCartStore();
+
+    $this->withHeader('Host', 'shop.test')
+        ->postJson('/api/storefront/v1/carts', ['currency' => 'USD'])
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors('currency');
+
+    $this->withHeader('Host', 'shop.test')
+        ->getJson('/api/storefront/v1/carts/999999')
+        ->assertNotFound();
+
+    $otherStoreCart = Cart::factory()->create(['store_id' => Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()]);
+
+    $this->withHeader('Host', 'shop.test')
+        ->getJson("/api/storefront/v1/carts/{$otherStoreCart->getKey()}")
+        ->assertNotFound();
+});
*** Delete File: tests/Feature/Api/StorefrontCheckoutApiTest.php
*** Add File: tests/Feature/Api/StorefrontCheckoutApiTest.php
+<?php
+
+use App\Enums\CheckoutStatus;
+use App\Models\Checkout;
+use App\Models\InventoryItem;
+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\ShippingRate;
+use App\Models\Store;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function storefrontApiCheckoutStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function storefrontApiCheckoutVariant(Store $store): ProductVariant
+{
+    $product = Product::withoutGlobalScopes()
+        ->where('store_id', $store->getKey())
+        ->where('handle', 'classic-cotton-t-shirt')
+        ->firstOrFail();
+
+    $variant = ProductVariant::withoutGlobalScopes()
+        ->where('product_id', $product->getKey())
+        ->oldest('position')
+        ->firstOrFail();
+
+    InventoryItem::withoutGlobalScopes()
+        ->where('variant_id', $variant->getKey())
+        ->update([
+            'quantity_on_hand' => 20,
+            'quantity_reserved' => 0,
+        ]);
+
+    return $variant->refresh();
+}
+
+/**
+ * @return array<string, string>
+ */
+function storefrontApiCheckoutAddress(string $country = 'DE'): array
+{
+    return [
+        'first_name' => 'Test',
+        'last_name' => 'Buyer',
+        'address1' => 'Main Street 1',
+        'city' => 'Berlin',
+        'province_code' => 'BE',
+        'country' => $country,
+        'country_code' => $country,
+        'postal_code' => '10115',
+    ];
+}
+
+test('storefront checkout api progresses through address shipping discount removal and payment selection', function (): void {
+    $store = storefrontApiCheckoutStore();
+    $variant = storefrontApiCheckoutVariant($store);
+    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.21'])->withHeader('Host', 'shop.test');
+
+    $cartId = $api()
+        ->postJson('/api/storefront/v1/carts')
+        ->assertCreated()['data']['id'];
+
+    $api()
+        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
+            'variant_id' => $variant->getKey(),
+            'quantity' => 2,
+        ])
+        ->assertCreated();
+
+    $checkoutResponse = $api()
+        ->postJson('/api/storefront/v1/checkouts', [
+            'cart_id' => $cartId,
+            'email' => 'buyer@example.test',
+        ]);
+
+    $checkoutResponse
+        ->assertCreated()
+        ->assertJsonPath('data.status', 'started')
+        ->assertJsonPath('data.email', 'buyer@example.test')
+        ->assertJsonPath('data.totals.subtotal', 5000)
+        ->assertJsonPath('data.cart.line_count', 2);
+
+    $checkoutId = $checkoutResponse['data']['id'];
+
+    $addressResponse = $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
+            'shipping_address' => storefrontApiCheckoutAddress(),
+        ]);
+
+    $addressResponse
+        ->assertOk()
+        ->assertJsonPath('data.status', 'addressed')
+        ->assertJsonPath('data.shipping_address.country', 'DE')
+        ->assertJsonCount(3, 'data.available_shipping_rates');
+
+    $shippingRateId = $addressResponse['data']['available_shipping_rates'][0]['id'];
+
+    $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
+            'shipping_rate_id' => $shippingRateId,
+        ])
+        ->assertOk()
+        ->assertJsonPath('data.status', 'shipping_selected')
+        ->assertJsonPath('data.shipping_method_id', $shippingRateId)
+        ->assertJsonPath('data.totals.shipping', 799);
+
+    $api()
+        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'SAVE10'])
+        ->assertOk()
+        ->assertJsonPath('data.discount_code', 'SAVE10')
+        ->assertJsonPath('data.totals.discount', 500);
+
+    $api()
+        ->deleteJson("/api/storefront/v1/checkouts/{$checkoutId}/discount")
+        ->assertOk()
+        ->assertJsonPath('data.discount_code', null)
+        ->assertJsonPath('data.totals.discount', 0);
+
+    $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/payment-method", [
+            'payment_method' => 'credit_card',
+        ])
+        ->assertOk()
+        ->assertJsonPath('data.status', 'payment_selected')
+        ->assertJsonPath('data.payment_method', 'credit_card');
+
+    $checkout = Checkout::withoutGlobalScopes()->findOrFail($checkoutId);
+    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
+
+    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
+        ->and($inventory->quantity_reserved)->toBe(2);
+});
+
+test('storefront checkout api rejects invalid addresses shipping methods and discounts', function (): void {
+    $store = storefrontApiCheckoutStore();
+    $variant = storefrontApiCheckoutVariant($store);
+    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.22'])->withHeader('Host', 'shop.test');
+
+    $cartId = $api()
+        ->postJson('/api/storefront/v1/carts')
+        ->assertCreated()['data']['id'];
+
+    $api()
+        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
+            'variant_id' => $variant->getKey(),
+            'quantity' => 1,
+        ])
+        ->assertCreated();
+
+    $checkoutId = $api()
+        ->postJson('/api/storefront/v1/checkouts', [
+            'cart_id' => $cartId,
+            'email' => 'buyer@example.test',
+        ])
+        ->assertCreated()['data']['id'];
+
+    $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
+            'shipping_address' => array_diff_key(storefrontApiCheckoutAddress(), ['first_name' => true]),
+        ])
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors('shipping_address.first_name');
+
+    $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
+            'shipping_address' => storefrontApiCheckoutAddress(),
+        ])
+        ->assertOk();
+
+    $otherStoreRate = ShippingRate::withoutGlobalScopes()
+        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()))
+        ->firstOrFail();
+
+    $api()
+        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
+            'shipping_rate_id' => $otherStoreRate->getKey(),
+        ])
+        ->assertUnprocessable();
+
+    $api()
+        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'NOTREAL'])
+        ->assertUnprocessable()
+        ->assertJsonPath('reason', 'not_found');
+});
*** End Patch


stdout:
Success. Updated the following files:
A tests/Feature/Api/StorefrontCartApiTest.php
A tests/Feature/Api/StorefrontCheckoutApiTest.php
D tests/Feature/Api/StorefrontCartApiTest.php
D tests/Feature/Api/StorefrontCheckoutApiTest.php

type: add

content:
<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

    return $variant->refresh();
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

content:
<?php

use App\Enums\CheckoutStatus;
use App\Models\Checkout;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

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

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

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

    return $variant->refresh();
}

/**
 * @return array<string, string>
 */
function storefrontApiCheckoutAddress(string $country = 'DE'): array
{
    return [
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Main Street 1',
        'city' => 'Berlin',
        'province_code' => 'BE',
        'country' => $country,
        'country_code' => $country,
        'postal_code' => '10115',
    ];
}

test('storefront checkout api progresses through address shipping discount removal and payment selection', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.21'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 2,
        ])
        ->assertCreated();

    $checkoutResponse = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ]);

    $checkoutResponse
        ->assertCreated()
        ->assertJsonPath('data.status', 'started')
        ->assertJsonPath('data.email', 'buyer@example.test')
        ->assertJsonPath('data.totals.subtotal', 5000)
        ->assertJsonPath('data.cart.line_count', 2);

    $checkoutId = $checkoutResponse['data']['id'];

    $addressResponse = $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ]);

    $addressResponse
        ->assertOk()
        ->assertJsonPath('data.status', 'addressed')
        ->assertJsonPath('data.shipping_address.country', 'DE')
        ->assertJsonCount(3, 'data.available_shipping_rates');

    $shippingRateId = $addressResponse['data']['available_shipping_rates'][0]['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $shippingRateId,
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'shipping_selected')
        ->assertJsonPath('data.shipping_method_id', $shippingRateId)
        ->assertJsonPath('data.totals.shipping', 799);

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'SAVE10'])
        ->assertOk()
        ->assertJsonPath('data.discount_code', 'SAVE10')
        ->assertJsonPath('data.totals.discount', 500);

    $api()
        ->deleteJson("/api/storefront/v1/checkouts/{$checkoutId}/discount")
        ->assertOk()
        ->assertJsonPath('data.discount_code', null)
        ->assertJsonPath('data.totals.discount', 0);

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/payment-method", [
            'payment_method' => 'credit_card',
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'payment_selected')
        ->assertJsonPath('data.payment_method', 'credit_card');

    $checkout = Checkout::withoutGlobalScopes()->findOrFail($checkoutId);
    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($inventory->quantity_reserved)->toBe(2);
});

test('storefront checkout api rejects invalid addresses shipping methods and discounts', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.22'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 1,
        ])
        ->assertCreated();

    $checkoutId = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ])
        ->assertCreated()['data']['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => array_diff_key(storefrontApiCheckoutAddress(), ['first_name' => true]),
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors('shipping_address.first_name');

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ])
        ->assertOk();

    $otherStoreRate = ShippingRate::withoutGlobalScopes()
        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()))
        ->firstOrFail();

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $otherStoreRate->getKey(),
        ])
        ->assertUnprocessable();

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'NOTREAL'])
        ->assertUnprocessable()
        ->assertJsonPath('reason', 'not_found');
});


output: {"output":"Success. Updated the following files:\nA tests/Feature/Api/StorefrontCartApiTest.php\nA tests/Feature/Api/StorefrontCheckoutApiTest.php\nD tests/Feature/Api/StorefrontCartApiTest.php\nD tests/Feature/Api/StorefrontCheckoutApiTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


cmd: sed -n '1,260p' database/seeders/ProductSeeder.php


cmd: sed -n '1,220p' database/seeders/StoreSeeder.php && sed -n '1,180p' database/seeders/StoreDomainSeeder.php


command: sed -n '1,260p' database/seeders/ProductSeeder.php

ProductSeeder.php

type: read

aggregated_output:
<?php

namespace Database\Seeders;

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

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

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

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

            $this->replaceCatalogChildren($product);

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

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

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

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

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

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

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

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

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

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

        return $optionValueIds;
    }

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

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

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

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

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

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

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

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

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

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

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

            $combinations = $next;
        }

        return $combinations;
    }

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

    /**
     * @param  array<string, string>  $selectedOptions
     */
    private function sku(string $prefix, array $selectedOptions): string
    {
        $parts = collect($selectedOptions)
            ->values()
            ->map(fn (string $value): string => $this->skuToken($value))
            ->all();

        return collect([$prefix, ...$parts])
            ->filter()
            ->implode('-');
    }

    private function skuToken(string $value): string
    {
        return [
            'White' => 'WHT',
            'Black' => 'BLK',
            'Navy' => 'NVY',
            'Blue' => 'BLU',
            'Brown' => 'BRN',
            'Olive' => 'OLV',
            'Sky Blue' => 'SKY',
            'Beige' => 'BGE',
            'Khaki' => 'KHK',
            'Sand' => 'SND',
            'Burgundy' => 'BUR',
            'Natural' => 'NAT',
            'Grey' => 'GRY',
            'Camel' => 'CAM',
            'Charcoal' => 'CHA',
            'Silver' => 'SLV',
            'Red' => 'RED',
            '25 EUR' => '25',
            '50 EUR' => '50',
            '100 EUR' => '100',
            '256GB' => '256',
            '512GB' => '512',


output:
Chunk ID: 9eb1a1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2217
Output:
<?php

namespace Database\Seeders;

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

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

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

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

            $this->replaceCatalogChildren($product);

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

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

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

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

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

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

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

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

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

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

        return $optionValueIds;
    }

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

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

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

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

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

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

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

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

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

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

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

            $combinations = $next;
        }

        return $combinations;
    }

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

    /**
     * @param  array<string, string>  $selectedOptions
     */
    private function sku(string $prefix, array $selectedOptions): string
    {
        $parts = collect($selectedOptions)
            ->values()
            ->map(fn (string $value): string => $this->skuToken($value))
            ->all();

        return collect([$prefix, ...$parts])
            ->filter()
            ->implode('-');
    }

    private function skuToken(string $value): string
    {
        return [
            'White' => 'WHT',
            'Black' => 'BLK',
            'Navy' => 'NVY',
            'Blue' => 'BLU',
            'Brown' => 'BRN',
            'Olive' => 'OLV',
            'Sky Blue' => 'SKY',
            'Beige' => 'BGE',
            'Khaki' => 'KHK',
            'Sand' => 'SND',
            'Burgundy' => 'BUR',
            'Natural' => 'NAT',
            'Grey' => 'GRY',
            'Camel' => 'CAM',
            'Charcoal' => 'CHA',
            'Silver' => 'SLV',
            'Red' => 'RED',
            '25 EUR' => '25',
            '50 EUR' => '50',
            '100 EUR' => '100',
            '256GB' => '256',
            '512GB' => '512',


command: sed -n '1,220p' database/seeders/StoreSeeder.php && sed -n '1,180p' database/seeders/StoreDomainSeeder.php

StoreDomainSeeder.php

aggregated_output:
<?php

namespace Database\Seeders;

use App\Models\Organization;
use App\Models\Store;
use Illuminate\Database\Seeder;

class StoreSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $organization = Organization::query()->where('billing_email', 'billing@acme.test')->firstOrFail();

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

namespace Database\Seeders;

use App\Models\Store;
use App\Models\StoreDomain;
use Illuminate\Database\Seeder;

class StoreDomainSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $domainsByStore = [
            'acme-fashion' => ['shop.test', 'acme-fashion.test'],
            'acme-electronics' => ['acme-electronics.test'],
        ];

        foreach ($domainsByStore as $storeHandle => $hostnames) {
            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();

            foreach ($hostnames as $index => $hostname) {
                StoreDomain::query()->updateOrCreate(
                    ['hostname' => $hostname],
                    [
                        'store_id' => $store->getKey(),
                        'type' => 'storefront',
                        'is_primary' => $index === 0,
                        'tls_mode' => 'managed',
                    ],
                );
            }
        }
    }
}


output:
Chunk ID: e27bfe
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 505
Output:
<?php

namespace Database\Seeders;

use App\Models\Organization;
use App\Models\Store;
use Illuminate\Database\Seeder;

class StoreSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $organization = Organization::query()->where('billing_email', 'billing@acme.test')->firstOrFail();

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

namespace Database\Seeders;

use App\Models\Store;
use App\Models\StoreDomain;
use Illuminate\Database\Seeder;

class StoreDomainSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $domainsByStore = [
            'acme-fashion' => ['shop.test', 'acme-fashion.test'],
            'acme-electronics' => ['acme-electronics.test'],
        ];

        foreach ($domainsByStore as $storeHandle => $hostnames) {
            $store = Store::query()->where('handle', $storeHandle)->firstOrFail();

            foreach ($hostnames as $index => $hostname) {
                StoreDomain::query()->updateOrCreate(
                    ['hostname' => $hostname],
                    [
                        'store_id' => $store->getKey(),
                        'type' => 'storefront',
                        'is_primary' => $index === 0,
                        'tls_mode' => 'managed',
                    ],
                );
            }
        }
    }
}


cmd: rg -n "classic-cotton-t-shirt|price_amount' => 2500|price_amount" database/seeders/ProductSeeder.php


command: rg -n "classic-cotton-t-shirt|price_amount' => 2500|price_amount" database/seeders/ProductSeeder.php

type: search

query: classic-cotton-t-shirt|price_amount' => 2500|price_amount

aggregated_output:
128:                'price_amount' => $variantData['price_amount'],
273:            $this->product('Classic Cotton T-Shirt', 'classic-cotton-t-shirt', 'Acme Basics', 'T-Shirts', ['new', 'popular'], ['new-arrivals', 't-shirts'], '
A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear.
', [['Size' => ['S', 'M', 'L', 'XL']], ['Color' => ['White', 'Black', 'Navy']]], ['price_amount' => 2499, 'weight_g' => 200, 'quantity_on_hand' => 15], 'ACME-CTSH'),
274:            $this->product('Premium Slim Fit Jeans', 'premium-slim-fit-jeans', 'Acme Denim', 'Pants', ['new', 'sale'], ['new-arrivals', 'pants-jeans', 'sale'], '
Slim fit jeans crafted from premium stretch denim. Comfortable all-day wear with a modern silhouette.
', [['Size' => ['28', '30', '32', '34', '36']], ['Color' => ['Blue', 'Black']]], ['price_amount' => 7999, 'compare_at_amount' => 9999, 'weight_g' => 800, 'quantity_on_hand' => 8], 'ACME-JNS'),
275:            $this->product('Organic Hoodie', 'organic-hoodie', 'Acme Basics', 'Hoodies', ['new', 'trending'], ['new-arrivals'], '
Made from 100% organic cotton. Warm, soft, and sustainably produced.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 5999, 'weight_g' => 500, 'quantity_on_hand' => 20], 'ACME-HOOD'),
276:            $this->product('Leather Belt', 'leather-belt', 'Acme Accessories', 'Accessories', ['popular'], [], '
Genuine leather belt with brushed metal buckle. A wardrobe essential.
', [['Size' => ['S/M', 'L/XL']], ['Color' => ['Brown', 'Black']]], ['price_amount' => 3499, 'weight_g' => 150, 'quantity_on_hand' => 25], 'ACME-BELT'),
277:            $this->product('Running Sneakers', 'running-sneakers', 'Acme Sport', 'Shoes', ['trending'], ['new-arrivals'], '
Lightweight running sneakers with responsive cushioning and breathable mesh upper.
', [['Size' => ['EU 38', 'EU 39', 'EU 40', 'EU 41', 'EU 42', 'EU 43', 'EU 44']], ['Color' => ['White', 'Black']]], ['price_amount' => 11999, 'weight_g' => 600, 'quantity_on_hand' => 5], 'ACME-RUN'),
278:            $this->product('Graphic Print Tee', 'graphic-print-tee', 'Acme Basics', 'T-Shirts', ['new'], ['t-shirts'], '
Bold graphic print on soft cotton. Express yourself with this statement piece.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 2999, 'weight_g' => 210, 'quantity_on_hand' => 18], 'ACME-GTEE'),
279:            $this->product('V-Neck Linen Tee', 'v-neck-linen-tee', 'Acme Basics', 'T-Shirts', ['popular'], ['t-shirts'], '
Lightweight linen blend v-neck. Perfect for warm summer days.
', [['Size' => ['S', 'M', 'L']], ['Color' => ['Beige', 'Olive', 'Sky Blue']]], ['price_amount' => 3499, 'weight_g' => 180, 'quantity_on_hand' => 12], 'ACME-VNECK'),
280:            $this->product('Striped Polo Shirt', 'striped-polo-shirt', 'Acme Basics', 'T-Shirts', ['sale'], ['t-shirts', 'sale'], '
Classic striped polo with a modern relaxed fit. Knitted collar and two-button placket.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 2799, 'compare_at_amount' => 3999, 'weight_g' => 250, 'quantity_on_hand' => 10], 'ACME-POLO'),
281:            $this->product('Cargo Pants', 'cargo-pants', 'Acme Workwear', 'Pants', ['popular'], ['pants-jeans'], '
Utility cargo pants with multiple pockets. Durable cotton twill construction.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Khaki', 'Olive', 'Black']]], ['price_amount' => 5499, 'weight_g' => 700, 'quantity_on_hand' => 14], 'ACME-CARGO'),
282:            $this->product('Chino Shorts', 'chino-shorts', 'Acme Basics', 'Pants', ['new', 'trending'], ['pants-jeans', 'new-arrivals'], '
Tailored chino shorts. Comfortable stretch fabric with a clean silhouette.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Navy', 'Sand']]], ['price_amount' => 3999, 'weight_g' => 350, 'quantity_on_hand' => 16], 'ACME-CHINO'),
283:            $this->product('Wide Leg Trousers', 'wide-leg-trousers', 'Acme Denim', 'Pants', ['sale'], ['pants-jeans', 'sale'], '
Relaxed wide leg trousers with a high waist. Flowing drape in premium woven fabric.
', [['Size' => ['S', 'M', 'L']]], ['price_amount' => 4999, 'compare_at_amount' => 6999, 'weight_g' => 550, 'quantity_on_hand' => 7], 'ACME-WIDE'),
284:            $this->product('Wool Scarf', 'wool-scarf', 'Acme Accessories', 'Accessories', ['popular'], [], '
Warm merino wool scarf. Soft hand feel, naturally breathable and temperature regulating.
', [['Color' => ['Grey', 'Burgundy', 'Navy']]], ['price_amount' => 2999, 'weight_g' => 120, 'quantity_on_hand' => 30], 'ACME-SCARF'),
285:            $this->product('Canvas Tote Bag', 'canvas-tote-bag', 'Acme Accessories', 'Accessories', ['trending'], [], '
Heavy-duty canvas tote bag with reinforced handles. Spacious enough for daily essentials.
', [['Color' => ['Natural', 'Black']]], ['price_amount' => 1999, 'weight_g' => 300, 'quantity_on_hand' => 40], 'ACME-TOTE'),
286:            $this->product('Bucket Hat', 'bucket-hat', 'Acme Accessories', 'Accessories', ['new', 'trending'], ['new-arrivals'], '
Lightweight bucket hat for sun protection. Packable design, washed cotton twill.
', [['Size' => ['S/M', 'L/XL']], ['Color' => ['Beige', 'Black', 'Olive']]], ['price_amount' => 2499, 'weight_g' => 80, 'quantity_on_hand' => 22], 'ACME-HAT'),
287:            $this->product('Unreleased Winter Jacket', 'unreleased-winter-jacket', 'Acme Outerwear', 'Jackets', ['limited'], [], '
Upcoming winter collection piece. Insulated puffer jacket with water-resistant shell.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 14999, 'weight_g' => 900, 'quantity_on_hand' => 0], 'ACME-WINTER', 'draft', null),
288:            $this->product('Discontinued Raincoat', 'discontinued-raincoat', 'Acme Outerwear', 'Jackets', [], [], '
Lightweight waterproof raincoat. This product has been discontinued.
', [['Size' => ['M', 'L']]], ['price_amount' => 8999, 'weight_g' => 400, 'quantity_on_hand' => 3], 'ACME-RAIN', 'archived', now()->subMonths(6)),
289:            $this->product('Limited Edition Sneakers', 'limited-edition-sneakers', 'Acme Sport', 'Shoes', ['limited'], [], '
Limited edition collaboration sneakers. Once they are gone, they are gone.
', [['Size' => ['EU 40', 'EU 42', 'EU 44']]], ['price_amount' => 15999, 'weight_g' => 650, 'quantity_on_hand' => 0, 'policy' => 'deny'], 'ACME-LIMITED'),
290:            $this->product('Backorder Denim Jacket', 'backorder-denim-jacket', 'Acme Denim', 'Jackets', ['popular'], [], '
Classic denim jacket. Currently on backorder - ships within 2-3 weeks.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 9999, 'weight_g' => 750, 'quantity_on_hand' => 0, 'policy' => 'continue'], 'ACME-BACKORDER'),
291:            $this->product('Gift Card', 'gift-card', 'Acme Fashion', 'Gift Cards', ['popular'], [], '
Digital gift card delivered via email. The perfect gift when you are not sure what to choose.
', [['Amount' => ['25 EUR', '50 EUR', '100 EUR']]], ['price_amount' => 2500, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999], 'ACME-GIFT', variants: [
292:                ['options' => ['Amount' => '25 EUR'], 'sku' => 'ACME-GIFT-25', 'price_amount' => 2500, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
293:                ['options' => ['Amount' => '50 EUR'], 'sku' => 'ACME-GIFT-50', 'price_amount' => 5000, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
294:                ['options' => ['Amount' => '100 EUR'], 'sku' => 'ACME-GIFT-100', 'price_amount' => 10000, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
296:            $this->product('Cashmere Overcoat', 'cashmere-overcoat', 'Acme Premium', 'Jackets', ['limited', 'new'], ['new-arrivals'], '
Luxurious cashmere-blend overcoat. Impeccable tailoring with silk lining.
', [['Size' => ['S', 'M', 'L']], ['Color' => ['Camel', 'Charcoal']]], ['price_amount' => 49999, 'weight_g' => 1200, 'quantity_on_hand' => 3], 'ACME-COAT'),
306:            $this->product('Pro Laptop 15', 'pro-laptop-15', 'TechCorp', 'Laptops', ['featured'], ['featured'], '
Performance laptop for tenant isolation tests.
', [['Storage' => ['256GB', '512GB', '1TB']]], ['price_amount' => 99999, 'weight_g' => 1800, 'quantity_on_hand' => 10], 'TECH-LAPTOP', variants: [
307:                ['options' => ['Storage' => '256GB'], 'sku' => 'TECH-LAPTOP-256', 'price_amount' => 99999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
308:                ['options' => ['Storage' => '512GB'], 'sku' => 'TECH-LAPTOP-512', 'price_amount' => 119999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
309:                ['options' => ['Storage' => '1TB'], 'sku' => 'TECH-LAPTOP-1TB', 'price_amount' => 149999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
311:            $this->product('Wireless Headphones', 'wireless-headphones', 'AudioMax', 'Audio', ['featured'], ['featured'], '
Wireless headphones for tenant isolation tests.
', [['Color' => ['Black', 'Silver']]], ['price_amount' => 14999, 'weight_g' => 250, 'quantity_on_hand' => 25], 'TECH-HEADPHONES'),
312:            $this->product('USB-C Cable 2m', 'usb-c-cable-2m', 'CablePro', 'Cables', ['accessories'], ['accessories'], '
Two meter USB-C cable.
', [], ['price_amount' => 1299, 'weight_g' => 50, 'quantity_on_hand' => 200], 'TECH-CABLE'),
313:            $this->product('Mechanical Keyboard', 'mechanical-keyboard', 'KeyTech', 'Peripherals', ['featured'], ['featured'], '
Mechanical keyboard with selectable switch types.
', [['Switch Type' => ['Red', 'Blue', 'Brown']]], ['price_amount' => 12999, 'weight_g' => 1100, 'quantity_on_hand' => 15], 'TECH-KEYBOARD'),
314:            $this->product('Monitor Stand', 'monitor-stand', 'DeskGear', 'Accessories', ['accessories'], ['accessories'], '
Desktop monitor stand.
', [], ['price_amount' => 4999, 'weight_g' => 2500, 'quantity_on_hand' => 30], 'TECH-STAND'),


output:
Chunk ID: c39006
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2495
Output:
128:                'price_amount' => $variantData['price_amount'],
273:            $this->product('Classic Cotton T-Shirt', 'classic-cotton-t-shirt', 'Acme Basics', 'T-Shirts', ['new', 'popular'], ['new-arrivals', 't-shirts'], '
A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear.
', [['Size' => ['S', 'M', 'L', 'XL']], ['Color' => ['White', 'Black', 'Navy']]], ['price_amount' => 2499, 'weight_g' => 200, 'quantity_on_hand' => 15], 'ACME-CTSH'),
274:            $this->product('Premium Slim Fit Jeans', 'premium-slim-fit-jeans', 'Acme Denim', 'Pants', ['new', 'sale'], ['new-arrivals', 'pants-jeans', 'sale'], '
Slim fit jeans crafted from premium stretch denim. Comfortable all-day wear with a modern silhouette.
', [['Size' => ['28', '30', '32', '34', '36']], ['Color' => ['Blue', 'Black']]], ['price_amount' => 7999, 'compare_at_amount' => 9999, 'weight_g' => 800, 'quantity_on_hand' => 8], 'ACME-JNS'),
275:            $this->product('Organic Hoodie', 'organic-hoodie', 'Acme Basics', 'Hoodies', ['new', 'trending'], ['new-arrivals'], '
Made from 100% organic cotton. Warm, soft, and sustainably produced.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 5999, 'weight_g' => 500, 'quantity_on_hand' => 20], 'ACME-HOOD'),
276:            $this->product('Leather Belt', 'leather-belt', 'Acme Accessories', 'Accessories', ['popular'], [], '
Genuine leather belt with brushed metal buckle. A wardrobe essential.
', [['Size' => ['S/M', 'L/XL']], ['Color' => ['Brown', 'Black']]], ['price_amount' => 3499, 'weight_g' => 150, 'quantity_on_hand' => 25], 'ACME-BELT'),
277:            $this->product('Running Sneakers', 'running-sneakers', 'Acme Sport', 'Shoes', ['trending'], ['new-arrivals'], '
Lightweight running sneakers with responsive cushioning and breathable mesh upper.
', [['Size' => ['EU 38', 'EU 39', 'EU 40', 'EU 41', 'EU 42', 'EU 43', 'EU 44']], ['Color' => ['White', 'Black']]], ['price_amount' => 11999, 'weight_g' => 600, 'quantity_on_hand' => 5], 'ACME-RUN'),
278:            $this->product('Graphic Print Tee', 'graphic-print-tee', 'Acme Basics', 'T-Shirts', ['new'], ['t-shirts'], '
Bold graphic print on soft cotton. Express yourself with this statement piece.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 2999, 'weight_g' => 210, 'quantity_on_hand' => 18], 'ACME-GTEE'),
279:            $this->product('V-Neck Linen Tee', 'v-neck-linen-tee', 'Acme Basics', 'T-Shirts', ['popular'], ['t-shirts'], '
Lightweight linen blend v-neck. Perfect for warm summer days.
', [['Size' => ['S', 'M', 'L']], ['Color' => ['Beige', 'Olive', 'Sky Blue']]], ['price_amount' => 3499, 'weight_g' => 180, 'quantity_on_hand' => 12], 'ACME-VNECK'),
280:            $this->product('Striped Polo Shirt', 'striped-polo-shirt', 'Acme Basics', 'T-Shirts', ['sale'], ['t-shirts', 'sale'], '
Classic striped polo with a modern relaxed fit. Knitted collar and two-button placket.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 2799, 'compare_at_amount' => 3999, 'weight_g' => 250, 'quantity_on_hand' => 10], 'ACME-POLO'),
281:            $this->product('Cargo Pants', 'cargo-pants', 'Acme Workwear', 'Pants', ['popular'], ['pants-jeans'], '
Utility cargo pants with multiple pockets. Durable cotton twill construction.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Khaki', 'Olive', 'Black']]], ['price_amount' => 5499, 'weight_g' => 700, 'quantity_on_hand' => 14], 'ACME-CARGO'),
282:            $this->product('Chino Shorts', 'chino-shorts', 'Acme Basics', 'Pants', ['new', 'trending'], ['pants-jeans', 'new-arrivals'], '
Tailored chino shorts. Comfortable stretch fabric with a clean silhouette.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Navy', 'Sand']]], ['price_amount' => 3999, 'weight_g' => 350, 'quantity_on_hand' => 16], 'ACME-CHINO'),
283:            $this->product('Wide Leg Trousers', 'wide-leg-trousers', 'Acme Denim', 'Pants', ['sale'], ['pants-jeans', 'sale'], '
Relaxed wide leg trousers with a high waist. Flowing drape in premium woven fabric.
', [['Size' => ['S', 'M', 'L']]], ['price_amount' => 4999, 'compare_at_amount' => 6999, 'weight_g' => 550, 'quantity_on_hand' => 7], 'ACME-WIDE'),
284:            $this->product('Wool Scarf', 'wool-scarf', 'Acme Accessories', 'Accessories', ['popular'], [], '
Warm merino wool scarf. Soft hand feel, naturally breathable and temperature regulating.
', [['Color' => ['Grey', 'Burgundy', 'Navy']]], ['price_amount' => 2999, 'weight_g' => 120, 'quantity_on_hand' => 30], 'ACME-SCARF'),
285:            $this->product('Canvas Tote Bag', 'canvas-tote-bag', 'Acme Accessories', 'Accessories', ['trending'], [], '
Heavy-duty canvas tote bag with reinforced handles. Spacious enough for daily essentials.
', [['Color' => ['Natural', 'Black']]], ['price_amount' => 1999, 'weight_g' => 300, 'quantity_on_hand' => 40], 'ACME-TOTE'),
286:            $this->product('Bucket Hat', 'bucket-hat', 'Acme Accessories', 'Accessories', ['new', 'trending'], ['new-arrivals'], '
Lightweight bucket hat for sun protection. Packable design, washed cotton twill.
', [['Size' => ['S/M', 'L/XL']], ['Color' => ['Beige', 'Black', 'Olive']]], ['price_amount' => 2499, 'weight_g' => 80, 'quantity_on_hand' => 22], 'ACME-HAT'),
287:            $this->product('Unreleased Winter Jacket', 'unreleased-winter-jacket', 'Acme Outerwear', 'Jackets', ['limited'], [], '
Upcoming winter collection piece. Insulated puffer jacket with water-resistant shell.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 14999, 'weight_g' => 900, 'quantity_on_hand' => 0], 'ACME-WINTER', 'draft', null),
288:            $this->product('Discontinued Raincoat', 'discontinued-raincoat', 'Acme Outerwear', 'Jackets', [], [], '
Lightweight waterproof raincoat. This product has been discontinued.
', [['Size' => ['M', 'L']]], ['price_amount' => 8999, 'weight_g' => 400, 'quantity_on_hand' => 3], 'ACME-RAIN', 'archived', now()->subMonths(6)),
289:            $this->product('Limited Edition Sneakers', 'limited-edition-sneakers', 'Acme Sport', 'Shoes', ['limited'], [], '
Limited edition collaboration sneakers. Once they are gone, they are gone.
', [['Size' => ['EU 40', 'EU 42', 'EU 44']]], ['price_amount' => 15999, 'weight_g' => 650, 'quantity_on_hand' => 0, 'policy' => 'deny'], 'ACME-LIMITED'),
290:            $this->product('Backorder Denim Jacket', 'backorder-denim-jacket', 'Acme Denim', 'Jackets', ['popular'], [], '
Classic denim jacket. Currently on backorder - ships within 2-3 weeks.
', [['Size' => ['S', 'M', 'L', 'XL']]], ['price_amount' => 9999, 'weight_g' => 750, 'quantity_on_hand' => 0, 'policy' => 'continue'], 'ACME-BACKORDER'),
291:            $this->product('Gift Card', 'gift-card', 'Acme Fashion', 'Gift Cards', ['popular'], [], '
Digital gift card delivered via email. The perfect gift when you are not sure what to choose.
', [['Amount' => ['25 EUR', '50 EUR', '100 EUR']]], ['price_amount' => 2500, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999], 'ACME-GIFT', variants: [
292:                ['options' => ['Amount' => '25 EUR'], 'sku' => 'ACME-GIFT-25', 'price_amount' => 2500, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
293:                ['options' => ['Amount' => '50 EUR'], 'sku' => 'ACME-GIFT-50', 'price_amount' => 5000, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
294:                ['options' => ['Amount' => '100 EUR'], 'sku' => 'ACME-GIFT-100', 'price_amount' => 10000, 'weight_g' => 0, 'requires_shipping' => false, 'quantity_on_hand' => 9999],
296:            $this->product('Cashmere Overcoat', 'cashmere-overcoat', 'Acme Premium', 'Jackets', ['limited', 'new'], ['new-arrivals'], '
Luxurious cashmere-blend overcoat. Impeccable tailoring with silk lining.
', [['Size' => ['S', 'M', 'L']], ['Color' => ['Camel', 'Charcoal']]], ['price_amount' => 49999, 'weight_g' => 1200, 'quantity_on_hand' => 3], 'ACME-COAT'),
306:            $this->product('Pro Laptop 15', 'pro-laptop-15', 'TechCorp', 'Laptops', ['featured'], ['featured'], '
Performance laptop for tenant isolation tests.
', [['Storage' => ['256GB', '512GB', '1TB']]], ['price_amount' => 99999, 'weight_g' => 1800, 'quantity_on_hand' => 10], 'TECH-LAPTOP', variants: [
307:                ['options' => ['Storage' => '256GB'], 'sku' => 'TECH-LAPTOP-256', 'price_amount' => 99999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
308:                ['options' => ['Storage' => '512GB'], 'sku' => 'TECH-LAPTOP-512', 'price_amount' => 119999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
309:                ['options' => ['Storage' => '1TB'], 'sku' => 'TECH-LAPTOP-1TB', 'price_amount' => 149999, 'weight_g' => 1800, 'quantity_on_hand' => 10],
311:            $this->product('Wireless Headphones', 'wireless-headphones', 'AudioMax', 'Audio', ['featured'], ['featured'], '
Wireless headphones for tenant isolation tests.
', [['Color' => ['Black', 'Silver']]], ['price_amount' => 14999, 'weight_g' => 250, 'quantity_on_hand' => 25], 'TECH-HEADPHONES'),
312:            $this->product('USB-C Cable 2m', 'usb-c-cable-2m', 'CablePro', 'Cables', ['accessories'], ['accessories'], '
Two meter USB-C cable.
', [], ['price_amount' => 1299, 'weight_g' => 50, 'quantity_on_hand' => 200], 'TECH-CABLE'),
313:            $this->product('Mechanical Keyboard', 'mechanical-keyboard', 'KeyTech', 'Peripherals', ['featured'], ['featured'], '
Mechanical keyboard with selectable switch types.
', [['Switch Type' => ['Red', 'Blue', 'Brown']]], ['price_amount' => 12999, 'weight_g' => 1100, 'quantity_on_hand' => 15], 'TECH-KEYBOARD'),
314:            $this->product('Monitor Stand', 'monitor-stand', 'DeskGear', 'Accessories', ['accessories'], ['accessories'], '
Desktop monitor stand.
', [], ['price_amount' => 4999, 'weight_g' => 2500, 'quantity_on_hand' => 30], 'TECH-STAND'),


cmd: sed -n '1,360p' app/Services/DiscountService.php


command: sed -n '1,360p' app/Services/DiscountService.php

aggregated_output:
<?php

namespace App\Services;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Exceptions\InvalidDiscountException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Discount;
use App\Models\ProductVariant;
use App\Models\Store;
use App\ValueObjects\DiscountResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DiscountService
{
    public function validate(string $code, Store $store, Cart $cart): Discount
    {
        $normalizedCode = mb_strtolower(trim($code));

        $discount = Discount::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('type', DiscountType::Code)
            ->whereRaw('lower(code) = ?', [$normalizedCode])
            ->first();

        if (! $discount instanceof Discount) {
            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');
        }

        if ($discount->status !== DiscountStatus::Active) {
            throw InvalidDiscountException::because('discount_expired', 'Discount is not active.');
        }

        if ($discount->starts_at->isFuture()) {
            throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
        }

        if ($discount->ends_at !== null && $discount->ends_at->isPast()) {
            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
        }

        if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
        }

        $lines = $this->cartLines($cart);
        $subtotal = $lines->sum('line_subtotal_amount');
        $minimum = (int) data_get($discount->rules_json, 'min_purchase_amount', data_get($discount->rules_json, 'minimum_purchase', 0));

        if ($minimum > 0 && $subtotal < $minimum) {
            throw InvalidDiscountException::because('discount_min_purchase_not_met', 'Cart does not meet the minimum purchase amount.');
        }

        if ($this->qualifyingLines($discount, $lines)->isEmpty()) {
            throw InvalidDiscountException::because('discount_not_applicable', 'Discount does not apply to these cart lines.');
        }

        return $discount;
    }

    /**
     * @param  array<int, CartLine>  $lines
     */
    public function calculate(Discount $discount, int $subtotal, array $lines): DiscountResult
    {
        if ($discount->value_type === DiscountValueType::FreeShipping) {
            return new DiscountResult(0, [], true);
        }

        $qualifyingLines = $this->qualifyingLines($discount, collect($lines));
        $qualifyingSubtotal = $qualifyingLines->sum('line_subtotal_amount');

        if ($qualifyingSubtotal <= 0) {
            return new DiscountResult(0, []);
        }

        $discountAmount = match ($discount->value_type) {
            DiscountValueType::Percent => (int) round($qualifyingSubtotal * $discount->value_amount / 100),
            DiscountValueType::Fixed => min($discount->value_amount, $qualifyingSubtotal),
            DiscountValueType::FreeShipping => 0,
        };

        $remaining = $discountAmount;
        $allocations = [];
        $lastIndex = $qualifyingLines->keys()->last();

        foreach ($qualifyingLines as $index => $line) {
            if ($index === $lastIndex) {
                $allocations[$line->getKey()] = $remaining;

                continue;
            }

            $allocation = (int) round($discountAmount * $line->line_subtotal_amount / $qualifyingSubtotal);
            $allocations[$line->getKey()] = $allocation;
            $remaining -= $allocation;
        }

        return new DiscountResult($discountAmount, $allocations);
    }

    public function applyToCart(Cart $cart, Discount $discount): DiscountResult
    {
        return DB::transaction(function () use ($cart, $discount): DiscountResult {
            $lines = $this->cartLines($cart);
            $result = $this->calculate($discount, $lines->sum('line_subtotal_amount'), $lines->all());

            $lines->each(function (CartLine $line) use ($result): void {
                $discountAmount = $result->allocations[$line->getKey()] ?? 0;

                $line->forceFill([
                    'line_discount_amount' => $discountAmount,
                    'line_total_amount' => $line->line_subtotal_amount - $discountAmount,
                ])->save();
            });

            return $result;
        });
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  Collection<int, CartLine>  $lines
     * @return Collection<int, CartLine>
     */
    private function qualifyingLines(Discount $discount, Collection $lines): Collection
    {
        $productIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();
        $collectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();

        if ($productIds->isEmpty() && $collectionIds->isEmpty()) {
            return $lines;
        }

        return $lines->filter(function (CartLine $line) use ($productIds, $collectionIds): bool {
            $variant = ProductVariant::withoutGlobalScopes()
                ->with(['product' => fn ($query) => $query->withoutGlobalScopes()->with('collections')])
                ->find($line->variant_id);

            if (! $variant instanceof ProductVariant || $variant->product === null) {
                return false;
            }

            if ($productIds->contains((int) $variant->product_id)) {
                return true;
            }

            return $variant->product->collections
                ->pluck('id')
                ->map(fn (int $id): int => $id)
                ->intersect($collectionIds)
                ->isNotEmpty();
        });
    }
}


output:
Chunk ID: 746220
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1594
Output:
<?php

namespace App\Services;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Exceptions\InvalidDiscountException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Discount;
use App\Models\ProductVariant;
use App\Models\Store;
use App\ValueObjects\DiscountResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DiscountService
{
    public function validate(string $code, Store $store, Cart $cart): Discount
    {
        $normalizedCode = mb_strtolower(trim($code));

        $discount = Discount::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('type', DiscountType::Code)
            ->whereRaw('lower(code) = ?', [$normalizedCode])
            ->first();

        if (! $discount instanceof Discount) {
            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');
        }

        if ($discount->status !== DiscountStatus::Active) {
            throw InvalidDiscountException::because('discount_expired', 'Discount is not active.');
        }

        if ($discount->starts_at->isFuture()) {
            throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
        }

        if ($discount->ends_at !== null && $discount->ends_at->isPast()) {
            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
        }

        if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
        }

        $lines = $this->cartLines($cart);
        $subtotal = $lines->sum('line_subtotal_amount');
        $minimum = (int) data_get($discount->rules_json, 'min_purchase_amount', data_get($discount->rules_json, 'minimum_purchase', 0));

        if ($minimum > 0 && $subtotal < $minimum) {
            throw InvalidDiscountException::because('discount_min_purchase_not_met', 'Cart does not meet the minimum purchase amount.');
        }

        if ($this->qualifyingLines($discount, $lines)->isEmpty()) {
            throw InvalidDiscountException::because('discount_not_applicable', 'Discount does not apply to these cart lines.');
        }

        return $discount;
    }

    /**
     * @param  array<int, CartLine>  $lines
     */
    public function calculate(Discount $discount, int $subtotal, array $lines): DiscountResult
    {
        if ($discount->value_type === DiscountValueType::FreeShipping) {
            return new DiscountResult(0, [], true);
        }

        $qualifyingLines = $this->qualifyingLines($discount, collect($lines));
        $qualifyingSubtotal = $qualifyingLines->sum('line_subtotal_amount');

        if ($qualifyingSubtotal <= 0) {
            return new DiscountResult(0, []);
        }

        $discountAmount = match ($discount->value_type) {
            DiscountValueType::Percent => (int) round($qualifyingSubtotal * $discount->value_amount / 100),
            DiscountValueType::Fixed => min($discount->value_amount, $qualifyingSubtotal),
            DiscountValueType::FreeShipping => 0,
        };

        $remaining = $discountAmount;
        $allocations = [];
        $lastIndex = $qualifyingLines->keys()->last();

        foreach ($qualifyingLines as $index => $line) {
            if ($index === $lastIndex) {
                $allocations[$line->getKey()] = $remaining;

                continue;
            }

            $allocation = (int) round($discountAmount * $line->line_subtotal_amount / $qualifyingSubtotal);
            $allocations[$line->getKey()] = $allocation;
            $remaining -= $allocation;
        }

        return new DiscountResult($discountAmount, $allocations);
    }

    public function applyToCart(Cart $cart, Discount $discount): DiscountResult
    {
        return DB::transaction(function () use ($cart, $discount): DiscountResult {
            $lines = $this->cartLines($cart);
            $result = $this->calculate($discount, $lines->sum('line_subtotal_amount'), $lines->all());

            $lines->each(function (CartLine $line) use ($result): void {
                $discountAmount = $result->allocations[$line->getKey()] ?? 0;

                $line->forceFill([
                    'line_discount_amount' => $discountAmount,
                    'line_total_amount' => $line->line_subtotal_amount - $discountAmount,
                ])->save();
            });

            return $result;
        });
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  Collection<int, CartLine>  $lines
     * @return Collection<int, CartLine>
     */
    private function qualifyingLines(Discount $discount, Collection $lines): Collection
    {
        $productIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();
        $collectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();

        if ($productIds->isEmpty() && $collectionIds->isEmpty()) {
            return $lines;
        }

        return $lines->filter(function (CartLine $line) use ($productIds, $collectionIds): bool {
            $variant = ProductVariant::withoutGlobalScopes()
                ->with(['product' => fn ($query) => $query->withoutGlobalScopes()->with('collections')])
                ->find($line->variant_id);

            if (! $variant instanceof ProductVariant || $variant->product === null) {
                return false;
            }

            if ($productIds->contains((int) $variant->product_id)) {
                return true;
            }

            return $variant->product->collections
                ->pluck('id')
                ->map(fn (int $id): int => $id)
                ->intersect($collectionIds)
                ->isNotEmpty();
        });
    }
}


status: completed

input:
*** Begin Patch
*** Update File: tests/Feature/Api/StorefrontCartApiTest.php
@@
         ->assertCreated()
         ->assertJsonPath('data.cart_version', 2)
         ->assertJsonPath('data.line_count', 2)
         ->assertJsonPath('data.lines.0.quantity', 2)
-        ->assertJsonPath('data.lines.0.line_total_amount', 5000);
+        ->assertJsonPath('data.lines.0.line_total_amount', 4998);
@@
         ->assertOk()
         ->assertJsonPath('data.cart_version', 3)
         ->assertJsonPath('data.lines.0.quantity', 3)
-        ->assertJsonPath('data.totals.total', 7500);
+        ->assertJsonPath('data.totals.total', 7497);
*** Update File: tests/Feature/Api/StorefrontCheckoutApiTest.php
@@
         ->assertCreated()
         ->assertJsonPath('data.status', 'started')
         ->assertJsonPath('data.email', 'buyer@example.test')
-        ->assertJsonPath('data.totals.subtotal', 5000)
+        ->assertJsonPath('data.totals.subtotal', 4998)
*** End Patch


stdout:
Success. Updated the following files:
M tests/Feature/Api/StorefrontCartApiTest.php
M tests/Feature/Api/StorefrontCheckoutApiTest.php

unified_diff:
@@ -89,3 +89,3 @@
         ->assertJsonPath('data.email', 'buyer@example.test')
-        ->assertJsonPath('data.totals.subtotal', 5000)
+        ->assertJsonPath('data.totals.subtotal', 4998)
         ->assertJsonPath('data.cart.line_count', 2);

unified_diff:
@@ -85,3 +85,3 @@
         ->assertJsonPath('data.lines.0.quantity', 2)
-        ->assertJsonPath('data.lines.0.line_total_amount', 5000);
+        ->assertJsonPath('data.lines.0.line_total_amount', 4998);

@@ -97,3 +97,3 @@
         ->assertJsonPath('data.lines.0.quantity', 3)
-        ->assertJsonPath('data.totals.total', 7500);
+        ->assertJsonPath('data.totals.total', 7497);


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


message: The core code and tests are now in place. I’m formatting the PHP changes, then I’ll run the focused API tests to catch any response-shape or middleware mismatches before broadening the suite.

phase: commentary


type: message

role: assistant

text: The core code and tests are now in place. I’m formatting the PHP changes, then I’ll run the focused API tests to catch any response-shape or middleware mismatches before broadening the suite.


output:
Chunk ID: a909ec
Wall time: 0.5061 seconds
Process exited with code 0
Original token count: 5
Output:
{"result":"pass"}


command:
/bin/zsh
-lc
vendor/bin/pint --dirty --format agent

type: unknown

cmd: vendor/bin/pint --dirty --format agent

result: pass


cmd: php artisan route:list --path=api/storefront/v1


cmd: php artisan test --compact tests/Feature/Api/StorefrontCartApiTest.php tests/Feature/Api/StorefrontCheckoutApiTest.php


output:
Chunk ID: 281782
Wall time: 0.2420 seconds
Process exited with code 0
Original token count: 265
Output:

  POST      api/storefront/v1/carts api.storefront.v1.carts.store › Api\Store…
  GET|HEAD  api/storefront/v1/carts/{cart} api.storefront.v1.carts.show › Api…
  POST      api/storefront/v1/carts/{cart}/lines api.storefront.v1.carts.line…
  PUT       api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  DELETE    api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  POST      api/storefront/v1/checkouts api.storefront.v1.checkouts.store › A…
  GET|HEAD  api/storefront/v1/checkouts/{checkout} api.storefront.v1.checkout…
  PUT       api/storefront/v1/checkouts/{checkout}/address api.storefront.v1.…
  POST      api/storefront/v1/checkouts/{checkout}/apply-discount api.storefr…
  DELETE    api/storefront/v1/checkouts/{checkout}/discount api.storefront.v1…
  PUT       api/storefront/v1/checkouts/{checkout}/payment-method api.storefr…
  PUT       api/storefront/v1/checkouts/{checkout}/shipping-method api.storef…

                                                           Showing [12] routes


command: php artisan route:list --path=api/storefront/v1

aggregated_output:
  POST      api/storefront/v1/carts api.storefront.v1.carts.store › Api\Store…
  GET|HEAD  api/storefront/v1/carts/{cart} api.storefront.v1.carts.show › Api…
  POST      api/storefront/v1/carts/{cart}/lines api.storefront.v1.carts.line…
  PUT       api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  DELETE    api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  POST      api/storefront/v1/checkouts api.storefront.v1.checkouts.store › A…
  GET|HEAD  api/storefront/v1/checkouts/{checkout} api.storefront.v1.checkout…
  PUT       api/storefront/v1/checkouts/{checkout}/address api.storefront.v1.…
  POST      api/storefront/v1/checkouts/{checkout}/apply-discount api.storefr…
  DELETE    api/storefront/v1/checkouts/{checkout}/discount api.storefront.v1…
  PUT       api/storefront/v1/checkouts/{checkout}/payment-method api.storefr…
  PUT       api/storefront/v1/checkouts/{checkout}/shipping-method api.storef…

                                                           Showing [12] routes