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

namespace App\Services\Tax;

use App\Contracts\TaxProvider;
use App\Models\TaxSettings;
use App\ValueObjects\TaxLine;
use App\ValueObjects\TaxResult;

class ManualTaxProvider implements TaxProvider
{
    /**
     * @param  array<int, int>  $amounts
     * @param  array<string, mixed>  $address
     */
    public function calculate(array $amounts, int $shippingAmount, TaxSettings $settings, array $address): TaxResult
    {
        $rate = $this->rateFor($settings, $address);

        if ($rate <= 0) {
            return new TaxResult([], 0, 0);
        }

        if ((bool) data_get($settings->config_json, 'shipping_taxable', true) && $shippingAmount > 0) {
            $amounts[] = $shippingAmount;
        }

        $taxAmount = collect($amounts)->sum(fn (int $amount): int => $settings->prices_include_tax
            ? $this->extractInclusive($amount, $rate)
            : $this->addExclusive($amount, $rate)
        );

        return new TaxResult([
            new TaxLine($this->nameFor($settings, $address), $rate, $taxAmount),
        ], $taxAmount, $rate);
    }

    /**
     * @param  array<string, mixed>  $address
     */
    public function rateFor(TaxSettings $settings, array $address): int
    {
        $country = strtoupper((string) (data_get($address, 'country_code') ?: data_get($address, 'country')));
        $province = strtoupper((string) data_get($address, 'province_code'));

        foreach (data_get($settings->config_json, 'rates', []) as $rate) {
            $rateCountry = strtoupper((string) data_get($rate, 'country'));
            $rateProvince = strtoupper((string) data_get($rate, 'province_code'));

            if ($rateCountry !== '' && $rateCountry !== $country) {
                continue;
            }

            if ($rateProvince !== '' && $rateProvince !== $province) {
                continue;
            }

            return (int) data_get($rate, 'rate_bps', 0);
        }

        return (int) data_get($settings->config_json, 'default_rate_bps', 0);
    }

    /**
     * @param  array<string, mixed>  $address
     */
    public function nameFor(TaxSettings $settings, array $address): string
    {
        $country = strtoupper((string) (data_get($address, 'country_code') ?: data_get($address, 'country')));

        foreach (data_get($settings->config_json, 'rates', []) as $rate) {
            if (strtoupper((string) data_get($rate, 'country')) === $country && data_get($rate, 'name')) {
                return (string) data_get($rate, 'name');
            }
        }

        return (string) data_get($settings->config_json, 'name', 'Tax');
    }

    public function extractInclusive(int $grossAmount, int $rateBasisPoints): int
    {
        if ($grossAmount <= 0 || $rateBasisPoints <= 0) {
            return 0;
        }

        return $grossAmount - intdiv($grossAmount * 10000, 10000 + $rateBasisPoints);
    }

    public function addExclusive(int $netAmount, int $rateBasisPoints): int
    {
        if ($netAmount <= 0 || $rateBasisPoints <= 0) {
            return 0;
        }

        return (int) round($netAmount * $rateBasisPoints / 10000);
    }
}


I’m going to tighten the API to the spec while adding tests: `cart_version` will be the public optimistic-lock field, cart line creation will return `201`, deletes will validate version conflicts, checkout routes will use the checkout limiter, and checkout creation will accept the email required by the API spec.


php artisan make:request Api/Storefront/V1/DestroyCartLineRequest --no-interaction


php artisan make:request Api/Storefront/V1/StoreCartRequest --no-interaction


Output:
Chunk ID: 083043
Wall time: 0.2144 seconds
Process exited with code 0
Original token count: 27
Output:

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


Output:
Chunk ID: 27a805
Wall time: 0.2087 seconds
Process exited with code 0
Original token count: 26
Output:

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


php artisan make:request Api/Storefront/V1/StoreCartRequest --no-interaction

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


php artisan make:request Api/Storefront/V1/DestroyCartLineRequest --no-interaction

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


sed -n '1,300p' app/Services/ShippingCalculator.php


sed -n '1,300p' app/Services/ShippingCalculator.php

<?php

namespace App\Services;

use App\Enums\ShippingRateType;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Support\Collection;

class ShippingCalculator
{
    /**
     * @param  array<string, mixed>  $address
     * @return Collection<int, ShippingRate>
     */
    public function getAvailableRates(Store $store, array $address): Collection
    {
        $zone = $this->matchingZone($store, $address);

        if (! $zone instanceof ShippingZone) {
            return collect();
        }

        return ShippingRate::withoutGlobalScopes()
            ->where('zone_id', $zone->getKey())
            ->where('is_active', true)
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  array<string, mixed>  $address
     */
    public function matchingZone(Store $store, array $address): ?ShippingZone
    {
        $country = strtoupper((string) (data_get($address, 'country_code') ?: data_get($address, 'country')));
        $region = strtoupper((string) data_get($address, 'province_code'));
        $bestZone = null;
        $bestSpecificity = -1;

        ShippingZone::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->get()
            ->each(function (ShippingZone $zone) use ($country, $region, &$bestZone, &$bestSpecificity): void {
                $countries = collect($zone->countries_json)->map(fn (string $code): string => strtoupper($code));
                $regions = collect($zone->regions_json)->map(fn (string $code): string => strtoupper($code));

                if (! $countries->contains($country)) {
                    return;
                }

                $specificity = $regions->contains($region) ? 2 : 1;

                if ($specificity > $bestSpecificity) {
                    $bestZone = $zone;
                    $bestSpecificity = $specificity;
                }
            });

        return $bestZone;
    }

    public function calculate(ShippingRate $rate, Cart $cart): ?int
    {
        if (! $this->requiresShipping($cart)) {
            return 0;
        }

        return match ($rate->type) {
            ShippingRateType::Flat => (int) data_get($rate->config_json, 'amount', 0),
            ShippingRateType::Weight => $this->weightRate($rate, $cart),
            ShippingRateType::Price => $this->priceRate($rate, $cart),
            ShippingRateType::Carrier => (int) data_get($rate->config_json, 'amount', 1299),
        };
    }

    public function requiresShipping(Cart $cart): bool
    {
        return $this->physicalLines($cart)->isNotEmpty();
    }

    private function weightRate(ShippingRate $rate, Cart $cart): ?int
    {
        $totalWeight = $this->physicalLines($cart)
            ->sum(function (CartLine $line): int {
                $variant = $this->variant($line);

                return ($variant?->weight_g ?? 0) * $line->quantity;
            });

        foreach (data_get($rate->config_json, 'ranges', []) as $range) {
            $minimum = (int) data_get($range, 'min_g', 0);
            $maximum = data_get($range, 'max_g');

            if ($totalWeight >= $minimum && ($maximum === null || $totalWeight <= (int) $maximum)) {
                return (int) data_get($range, 'amount', 0);
            }
        }

        return null;
    }

    private function priceRate(ShippingRate $rate, Cart $cart): ?int
    {
        $subtotal = CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->sum('line_subtotal_amount');

        foreach (data_get($rate->config_json, 'ranges', []) as $range) {
            $minimum = (int) data_get($range, 'min_amount', 0);
            $maximum = data_get($range, 'max_amount');

            if ($subtotal >= $minimum && ($maximum === null || $subtotal <= (int) $maximum)) {
                return (int) data_get($range, 'amount', 0);
            }
        }

        return null;
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function physicalLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->get()
            ->filter(function (CartLine $line): bool {
                return (bool) $this->variant($line)?->requires_shipping;
            });
    }

    private function variant(CartLine $line): ?ProductVariant
    {
        return ProductVariant::withoutGlobalScopes()
            ->whereKey($line->variant_id)
            ->first();
    }
}


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

namespace App\Services;

use App\Enums\ShippingRateType;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Support\Collection;

class ShippingCalculator
{
    /**
     * @param  array<string, mixed>  $address
     * @return Collection<int, ShippingRate>
     */
    public function getAvailableRates(Store $store, array $address): Collection
    {
        $zone = $this->matchingZone($store, $address);

        if (! $zone instanceof ShippingZone) {
            return collect();
        }

        return ShippingRate::withoutGlobalScopes()
            ->where('zone_id', $zone->getKey())
            ->where('is_active', true)
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  array<string, mixed>  $address
     */
    public function matchingZone(Store $store, array $address): ?ShippingZone
    {
        $country = strtoupper((string) (data_get($address, 'country_code') ?: data_get($address, 'country')));
        $region = strtoupper((string) data_get($address, 'province_code'));
        $bestZone = null;
        $bestSpecificity = -1;

        ShippingZone::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->get()
            ->each(function (ShippingZone $zone) use ($country, $region, &$bestZone, &$bestSpecificity): void {
                $countries = collect($zone->countries_json)->map(fn (string $code): string => strtoupper($code));
                $regions = collect($zone->regions_json)->map(fn (string $code): string => strtoupper($code));

                if (! $countries->contains($country)) {
                    return;
                }

                $specificity = $regions->contains($region) ? 2 : 1;

                if ($specificity > $bestSpecificity) {
                    $bestZone = $zone;
                    $bestSpecificity = $specificity;
                }
            });

        return $bestZone;
    }

    public function calculate(ShippingRate $rate, Cart $cart): ?int
    {
        if (! $this->requiresShipping($cart)) {
            return 0;
        }

        return match ($rate->type) {
            ShippingRateType::Flat => (int) data_get($rate->config_json, 'amount', 0),
            ShippingRateType::Weight => $this->weightRate($rate, $cart),
            ShippingRateType::Price => $this->priceRate($rate, $cart),
            ShippingRateType::Carrier => (int) data_get($rate->config_json, 'amount', 1299),
        };
    }

    public function requiresShipping(Cart $cart): bool
    {
        return $this->physicalLines($cart)->isNotEmpty();
    }

    private function weightRate(ShippingRate $rate, Cart $cart): ?int
    {
        $totalWeight = $this->physicalLines($cart)
            ->sum(function (CartLine $line): int {
                $variant = $this->variant($line);

                return ($variant?->weight_g ?? 0) * $line->quantity;
            });

        foreach (data_get($rate->config_json, 'ranges', []) as $range) {
            $minimum = (int) data_get($range, 'min_g', 0);
            $maximum = data_get($range, 'max_g');

            if ($totalWeight >= $minimum && ($maximum === null || $totalWeight <= (int) $maximum)) {
                return (int) data_get($range, 'amount', 0);
            }
        }

        return null;
    }

    private function priceRate(ShippingRate $rate, Cart $cart): ?int
    {
        $subtotal = CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->sum('line_subtotal_amount');

        foreach (data_get($rate->config_json, 'ranges', []) as $range) {
            $minimum = (int) data_get($range, 'min_amount', 0);
            $maximum = data_get($range, 'max_amount');

            if ($subtotal >= $minimum && ($maximum === null || $subtotal <= (int) $maximum)) {
                return (int) data_get($range, 'amount', 0);
            }
        }

        return null;
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function physicalLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->get()
            ->filter(function (CartLine $line): bool {
                return (bool) $this->variant($line)?->requires_shipping;
            });
    }

    private function variant(CartLine $line): ?ProductVariant
    {
        return ProductVariant::withoutGlobalScopes()
            ->whereKey($line->variant_id)
            ->first();
    }
}


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

+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

 class StoreCartRequest extends FormRequest
 {
@@
      */
     public function rules(): array
     {
+        $currencyRules = ['nullable', 'string', 'size:3'];
+        $store = app()->bound('current_store') ? app('current_store') : null;
+
+        if ($store instanceof Store) {
+            $currencyRules[] = Rule::in([$store->default_currency]);
+        }
+
         return [
-            //
+            'currency' => $currencyRules,
         ];
     }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php
@@
     public function rules(): array
     {
         return [
             'variant_id' => ['required', 'integer', 'exists:product_variants,id'],
             'quantity' => ['required', 'integer', 'min:1'],
+            'cart_version' => ['nullable', 'integer', 'min:1'],
             'expected_version' => ['nullable', 'integer', 'min:1'],
         ];
     }
+
+    public function expectedVersion(): ?int
+    {
+        $version = $this->validated('cart_version') ?? $this->validated('expected_version');
+
+        return $version === null ? null : (int) $version;
+    }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php
@@
     public function rules(): array
     {
         return [
             'quantity' => ['required', 'integer', 'min:0'],
-            'expected_version' => ['nullable', 'integer', 'min:1'],
+            'cart_version' => ['required_without:expected_version', 'integer', 'min:1'],
+            'expected_version' => ['required_without:cart_version', 'integer', 'min:1'],
         ];
     }
+
+    public function expectedVersion(): int
+    {
+        return (int) ($this->validated('cart_version') ?? $this->validated('expected_version'));
+    }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php
@@
 namespace App\Http\Requests\Api\Storefront\V1;

 use Illuminate\Foundation\Http\FormRequest;
@@
     public function rules(): array
     {
         return [
-            //
+            'cart_version' => ['required_without:expected_version', 'integer', 'min:1'],
+            'expected_version' => ['required_without:cart_version', 'integer', 'min:1'],
         ];
     }
+
+    public function expectedVersion(): int
+    {
+        return (int) ($this->validated('cart_version') ?? $this->validated('expected_version'));
+    }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php
@@
     public function rules(): array
     {
         return [
             'cart_id' => ['required', 'integer', 'exists:carts,id'],
+            'email' => ['required', 'email'],
         ];
     }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php
@@
     public function rules(): array
     {
         return [
-            'email' => ['required', 'email'],
+            'email' => ['nullable', 'email'],
             'shipping_address' => ['required', 'array'],
             'shipping_address.first_name' => ['required', 'string'],
             'shipping_address.last_name' => ['required', 'string'],
             'shipping_address.address1' => ['required', 'string'],
             'shipping_address.address2' => ['nullable', 'string'],
             'shipping_address.city' => ['required', 'string'],
             'shipping_address.province_code' => ['nullable', 'string'],
             'shipping_address.country' => ['required', 'string', 'size:2'],
+            'shipping_address.country_code' => ['nullable', 'string', 'size:2'],
             'shipping_address.postal_code' => ['required', 'string'],
             'billing_address' => ['nullable', 'array'],
         ];
     }
 }
*** Update File: app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php
@@
     public function rules(): array
     {
         return [
-            'code' => ['nullable', 'string', 'max:255'],
+            'code' => ['required', 'string', 'max:50'],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php
M app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php
M app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php
M app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php
M app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php
M app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php
M app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php

@@ -25,2 +25,3 @@
             'cart_id' => ['required', 'integer', 'exists:carts,id'],
+            'email' => ['required', 'email'],
         ];

@@ -24,3 +24,3 @@
         return [
-            'email' => ['required', 'email'],
+            'email' => ['nullable', 'email'],
             'shipping_address' => ['required', 'array'],
@@ -33,2 +33,3 @@
             'shipping_address.country' => ['required', 'string', 'size:2'],
+            'shipping_address.country_code' => ['nullable', 'string', 'size:2'],
             'shipping_address.postal_code' => ['required', 'string'],

@@ -24,5 +24,11 @@
         return [
-            //
+            'cart_version' => ['required_without:expected_version', 'integer', 'min:1'],
+            'expected_version' => ['required_without:cart_version', 'integer', 'min:1'],
         ];
     }
+
+    public function expectedVersion(): int
+    {
+        return (int) ($this->validated('cart_version') ?? $this->validated('expected_version'));
+    }
 }

@@ -4,3 +4,5 @@

+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;

@@ -23,4 +25,11 @@
     {
+        $currencyRules = ['nullable', 'string', 'size:3'];
+        $store = app()->bound('current_store') ? app('current_store') : null;
+
+        if ($store instanceof Store) {
+            $currencyRules[] = Rule::in([$store->default_currency]);
+        }
+
         return [
-            //
+            'currency' => $currencyRules,
         ];

@@ -26,2 +26,3 @@
             'quantity' => ['required', 'integer', 'min:1'],
+            'cart_version' => ['nullable', 'integer', 'min:1'],
             'expected_version' => ['nullable', 'integer', 'min:1'],
@@ -29,2 +30,9 @@
     }
+
+    public function expectedVersion(): ?int
+    {
+        $version = $this->validated('cart_version') ?? $this->validated('expected_version');
+
+        return $version === null ? null : (int) $version;
+    }
 }

@@ -25,5 +25,11 @@
             'quantity' => ['required', 'integer', 'min:0'],
-            'expected_version' => ['nullable', 'integer', 'min:1'],
+            'cart_version' => ['required_without:expected_version', 'integer', 'min:1'],
+            'expected_version' => ['required_without:cart_version', 'integer', 'min:1'],
         ];
     }
+
+    public function expectedVersion(): int
+    {
+        return (int) ($this->validated('cart_version') ?? $this->validated('expected_version'));
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php\nM app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php\nM app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php\nM app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php\nM app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php\nM app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php\nM app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Storefront/V1/CartController.php
@@
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
 use App\Http\Resources\Storefront\V1\CartResource;
 use App\Models\Cart;
 use App\Models\Store;
@@
 class CartController extends Controller
 {
-    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())))
             ->response()
             ->setStatusCode(201);
*** Update File: app/Http/Controllers/Api/Storefront/V1/CartLineController.php
@@
 use App\Exceptions\InsufficientInventoryException;
 use App\Exceptions\InvalidCartOperationException;
 use App\Http\Controllers\Controller;
+use App\Http\Requests\Api\Storefront\V1\DestroyCartLineRequest;
 use App\Http\Requests\Api\Storefront\V1\StoreCartLineRequest;
 use App\Http\Requests\Api\Storefront\V1\UpdateCartLineRequest;
@@
                 $cart,
                 (int) $request->validated('variant_id'),
                 (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) {
             return $this->versionMismatch($exception, $cart);
         } catch (InsufficientInventoryException|InvalidCartOperationException $exception) {
@@
                 $cart,
                 $cartLine->getKey(),
                 (int) $request->validated('quantity'),
-                $request->validated('expected_version'),
+                $request->expectedVersion(),
             );

             return CartResource::make($this->loadCart($cart->refresh()));
@@
-    public function destroy(Cart $cart, CartLine $cartLine, CartService $carts): CartResource
+    public function destroy(DestroyCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
     {
         abort_unless((int) $cartLine->cart_id === (int) $cart->getKey(), 404);

-        $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);
+        }

         return CartResource::make($this->loadCart($cart->refresh()));
     }
@@
         return response()->json([
             'message' => $exception->getMessage(),
+            'expected_cart_version' => $exception->expectedVersion,
+            'current_cart_version' => $exception->currentVersion,
             'cart' => CartResource::make($this->loadCart($cart->refresh())),
         ], 409);
     }
*** Update File: app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
@@
     public function store(StoreCheckoutRequest $request, CheckoutService $checkouts): CheckoutResource|JsonResponse
     {
         $cart = Cart::query()->findOrFail($request->validated('cart_id'));

         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);
         } catch (InvalidCheckoutTransitionException $exception) {
             return response()->json(['message' => $exception->getMessage()], 422);
         }
@@
     public function address(SetCheckoutAddressRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
     {
+        $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) {
             return response()->json(['message' => $exception->getMessage()], 422);
         }
@@
             ], 422);
         }
     }
+
+    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
     {
*** Update File: routes/api.php
@@
 Route::middleware(['store.resolve', 'throttle:api.storefront'])
     ->prefix('storefront/v1')
     ->name('api.storefront.v1.')
     ->group(function (): void {
         Route::post('carts', [CartController::class, 'store'])->name('carts.store');
@@
-        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');
+        });
     });
*** Update File: app/Providers/AppServiceProvider.php
@@
         });

         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());
         });
*** End Patch


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

@@ -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())))

@@ -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

@@ -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());
         });

@@ -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())),

@@ -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}}


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


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

@@ -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');
+        });


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


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


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

@@ -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}}


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


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

<?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();
});

<?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}}


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


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


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

<?php

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
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',


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

StoreDomainSeeder.php

<?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',
                    ],
                );
            }
        }
    }
}


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


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

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

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'),