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

namespace App\Livewire\Storefront\Account\Auth;

use App\Models\Customer;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Livewire\Component;

class Login extends Component
{
    public int $storeId;

    public string $email = '';

    public string $password = '';

    public bool $remember = false;

    public function mount(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeId = $store->getKey();
    }

    public function login(): void
    {
        $this->validate([
            'email' => ['required', 'email'],
            'password' => ['required', 'string'],
            'remember' => ['boolean'],
        ]);

        $key = 'customer-login:'.request()->ip();

        if (RateLimiter::tooManyAttempts($key, 5)) {
            throw ValidationException::withMessages([
                'email' => __('Too many attempts. Try again in :seconds seconds.', [
                    'seconds' => RateLimiter::availableIn($key),
                ]),
            ]);
        }

        app()->instance('current_store', Store::query()->findOrFail($this->storeId));

        if (! Auth::guard('customer')->attempt([
            'email' => $this->email,
            'password' => $this->password,
        ], $this->remember)) {
            RateLimiter::hit($key, 60);

            throw ValidationException::withMessages([
                'email' => __('Invalid credentials'),
            ]);
        }

        RateLimiter::clear($key);

        if (request()->hasSession()) {
            request()->session()->regenerate();
        }

        $customer = Auth::guard('customer')->user();

        if ($customer instanceof Customer && session()->has('cart_id')) {
            app(CartService::class)->getOrCreateForSession(Store::query()->findOrFail($this->storeId), $customer);
        }

        $this->redirectRoute('account.dashboard', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.storefront.account.auth.login')
            ->layout('layouts.auth');
    }
}


output:
Chunk ID: 16929d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 300
Output:

    <x-auth-header :title="__('Log in')" :description="__('Access your orders and saved addresses')" />

    <form wire:submit="login" class="flex flex-col gap-6">
        <flux:input
            wire:model="email"
            :label="__('Email address')"
            type="email"
            required
            autofocus
            autocomplete="email"
            placeholder="customer@acme.test"
        />

        <flux:input
            wire:model="password"
            :label="__('Password')"
            type="password"
            required
            autocomplete="current-password"
            :placeholder="__('Password')"
            viewable
        />

        <flux:checkbox wire:model="remember" :label="__('Remember me')" />

        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-login-button">
            {{ __('Log in') }}
        </flux:button>
    </form>

        {{ __('New customer?') }}
        <flux:link :href="route('account.register')" wire:navigate>{{ __('Create an account') }}</flux:link>


output:
Chunk ID: af6633
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1289
Output:
<!DOCTYPE html>
getLocale()) }}">

        @include('partials.head')

        @php($store = app()->bound('current_store') ? app('current_store') : null)
        @php($announcement = data_get($themeSettings ?? [], 'announcement', []))
        @php($mainLinks = ($mainNavigation ?? []) !== [] ? $mainNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])
        @php($footerLinks = ($footerNavigation ?? []) !== [] ? $footerNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])

            Skip to main content

        @if (data_get($announcement, 'enabled', false))

                @if (data_get($announcement, 'url'))

                        {{ data_get($announcement, 'text') }}

                @else
                    {{ data_get($announcement, 'text') }}
                @endif

        @endif

         data_get($themeSettings ?? [], 'header.sticky', true),
        ])>

                    {{ $store?->name ?? config('app.name') }}

                    @foreach ($mainLinks as $item)

                            {{ $item['label'] }}

                    @endforeach

                    <flux:button :href="route('search.index')" wire:navigate variant="subtle" icon="magnifying-glass" aria-label="Search" />
                    <flux:button :href="route('account.dashboard')" wire:navigate variant="subtle" icon="user" aria-label="Account" />
                    <flux:modal.trigger name="cart-drawer">
                        <flux:button variant="subtle" icon="shopping-bag" aria-label="Cart" />
                    </flux:modal.trigger>

        <livewire:storefront.cart-drawer />

            {{ $slot }}

{{ $store?->name ?? config('app.name') }}

{{ data_get($themeSettings ?? [], 'footer.tagline') }}

Shop

                        @foreach ($footerLinks as $item)

                                {{ $item['label'] }}

                        @endforeach

Customer

                        Account
                        About

        @fluxScripts


cmd: sed -n '1,140p' app/Services/CartService.php


command: sed -n '1,140p' app/Services/CartService.php

CartService.php

type: read

aggregated_output:
<?php

namespace App\Services;

use App\Enums\CartStatus;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Exceptions\CartVersionMismatchException;
use App\Exceptions\InvalidCartOperationException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Support\Facades\DB;

class CartService
{
    public function __construct(
        private readonly InventoryService $inventory,
    ) {}

    public function create(Store $store, ?Customer $customer = null): Cart
    {
        return Cart::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            'customer_id' => $customer?->getKey(),
            'currency' => $store->default_currency,
            'cart_version' => 1,
            'status' => CartStatus::Active,
        ]);
    }

    public function getOrCreateForSession(Store $store, ?Customer $customer = null): Cart
    {
        $sessionCartId = session('cart_id');

        if ($customer instanceof Customer) {
            $customerCart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('customer_id', $customer->getKey())
                ->where('status', CartStatus::Active)
                ->latest('id')
                ->first() ?? $this->create($store, $customer);

            if ($sessionCartId) {
                $guestCart = Cart::withoutGlobalScopes()
                    ->where('store_id', $store->getKey())
                    ->whereNull('customer_id')
                    ->where('status', CartStatus::Active)
                    ->find($sessionCartId);

                if ($guestCart instanceof Cart && $guestCart->isNot($customerCart)) {
                    $customerCart = $this->mergeOnLogin($guestCart, $customerCart);
                }

                session()->forget('cart_id');
            }

            return $customerCart->refresh();
        }

        if ($sessionCartId) {
            $cart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->whereNull('customer_id')
                ->where('status', CartStatus::Active)
                ->find($sessionCartId);

            if ($cart instanceof Cart) {
                return $cart;
            }
        }

        $cart = $this->create($store);
        session(['cart_id' => $cart->getKey()]);

        return $cart;
    }

    public function currentForSession(Store $store, ?Customer $customer = null): ?Cart
    {
        if ($customer instanceof Customer) {
            $customerCart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('customer_id', $customer->getKey())
                ->where('status', CartStatus::Active)
                ->latest('id')
                ->first();

            if ($customerCart instanceof Cart) {
                return $customerCart;
            }
        }

        $sessionCartId = session('cart_id');

        if (! $sessionCartId) {
            return null;
        }

        return Cart::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('status', CartStatus::Active)
            ->where(function ($query) use ($customer): void {
                if ($customer instanceof Customer) {
                    $query
                        ->whereNull('customer_id')
                        ->orWhere('customer_id', $customer->getKey());

                    return;
                }

                $query->whereNull('customer_id');
            })
            ->find($sessionCartId);
    }

    public function addLine(Cart $cart, int $variantId, int $quantity, ?int $expectedVersion = null): CartLine
    {
        $this->assertPositiveQuantity($quantity);

        return DB::transaction(function () use ($cart, $variantId, $quantity, $expectedVersion): CartLine {
            $lockedCart = $this->freshCart($cart);
            $this->assertExpectedVersion($lockedCart, $expectedVersion);
            $this->assertCartIsActive($lockedCart);

            $variant = $this->purchasableVariant($lockedCart, $variantId);
            $line = CartLine::withoutGlobalScopes()
                ->where('cart_id', $lockedCart->getKey())
                ->where('variant_id', $variant->getKey())
                ->first();
            $newQuantity = ($line?->quantity ?? 0) + $quantity;

            $this->assertAvailable($variant->inventoryItem, $newQuantity);

            if (! $line instanceof CartLine) {
                $line = new CartLine([
                    'cart_id' => $lockedCart->getKey(),

status: completed


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

namespace App\Services;

use App\Enums\CartStatus;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Exceptions\CartVersionMismatchException;
use App\Exceptions\InvalidCartOperationException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Support\Facades\DB;

class CartService
{
    public function __construct(
        private readonly InventoryService $inventory,
    ) {}

    public function create(Store $store, ?Customer $customer = null): Cart
    {
        return Cart::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            'customer_id' => $customer?->getKey(),
            'currency' => $store->default_currency,
            'cart_version' => 1,
            'status' => CartStatus::Active,
        ]);
    }

    public function getOrCreateForSession(Store $store, ?Customer $customer = null): Cart
    {
        $sessionCartId = session('cart_id');

        if ($customer instanceof Customer) {
            $customerCart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('customer_id', $customer->getKey())
                ->where('status', CartStatus::Active)
                ->latest('id')
                ->first() ?? $this->create($store, $customer);

            if ($sessionCartId) {
                $guestCart = Cart::withoutGlobalScopes()
                    ->where('store_id', $store->getKey())
                    ->whereNull('customer_id')
                    ->where('status', CartStatus::Active)
                    ->find($sessionCartId);

                if ($guestCart instanceof Cart && $guestCart->isNot($customerCart)) {
                    $customerCart = $this->mergeOnLogin($guestCart, $customerCart);
                }

                session()->forget('cart_id');
            }

            return $customerCart->refresh();
        }

        if ($sessionCartId) {
            $cart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->whereNull('customer_id')
                ->where('status', CartStatus::Active)
                ->find($sessionCartId);

            if ($cart instanceof Cart) {
                return $cart;
            }
        }

        $cart = $this->create($store);
        session(['cart_id' => $cart->getKey()]);

        return $cart;
    }

    public function currentForSession(Store $store, ?Customer $customer = null): ?Cart
    {
        if ($customer instanceof Customer) {
            $customerCart = Cart::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('customer_id', $customer->getKey())
                ->where('status', CartStatus::Active)
                ->latest('id')
                ->first();

            if ($customerCart instanceof Cart) {
                return $customerCart;
            }
        }

        $sessionCartId = session('cart_id');

        if (! $sessionCartId) {
            return null;
        }

        return Cart::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('status', CartStatus::Active)
            ->where(function ($query) use ($customer): void {
                if ($customer instanceof Customer) {
                    $query
                        ->whereNull('customer_id')
                        ->orWhere('customer_id', $customer->getKey());

                    return;
                }

                $query->whereNull('customer_id');
            })
            ->find($sessionCartId);
    }

    public function addLine(Cart $cart, int $variantId, int $quantity, ?int $expectedVersion = null): CartLine
    {
        $this->assertPositiveQuantity($quantity);

        return DB::transaction(function () use ($cart, $variantId, $quantity, $expectedVersion): CartLine {
            $lockedCart = $this->freshCart($cart);
            $this->assertExpectedVersion($lockedCart, $expectedVersion);
            $this->assertCartIsActive($lockedCart);

            $variant = $this->purchasableVariant($lockedCart, $variantId);
            $line = CartLine::withoutGlobalScopes()
                ->where('cart_id', $lockedCart->getKey())
                ->where('variant_id', $variant->getKey())
                ->first();
            $newQuantity = ($line?->quantity ?? 0) + $quantity;

            $this->assertAvailable($variant->inventoryItem, $newQuantity);

            if (! $line instanceof CartLine) {
                $line = new CartLine([
                    'cart_id' => $lockedCart->getKey(),


cmd: php artisan make:livewire Storefront/Checkout/Confirmation --class --no-interaction


cmd: php artisan make:livewire Storefront/Account/Orders/Index --class --no-interaction


cmd: php artisan make:livewire Storefront/Account/Orders/Show --class --no-interaction


cmd: php artisan make:test --pest Storefront/OrderViewsTest --no-interaction


output:
Chunk ID: 487c6e
Wall time: 0.1978 seconds
Process exited with code 0
Original token count: 27
Output:

   INFO  Livewire component [app/Livewire/Storefront/Checkout/Confirmation.php] created successfully.


output:
Chunk ID: 35c4f9
Wall time: 0.1938 seconds
Process exited with code 0
Original token count: 27
Output:

   INFO  Livewire component [app/Livewire/Storefront/Account/Orders/Index.php] created successfully.


output:
Chunk ID: fc22cb
Wall time: 0.1866 seconds
Process exited with code 0
Original token count: 26
Output:

   INFO  Livewire component [app/Livewire/Storefront/Account/Orders/Show.php] created successfully.


output:
Chunk ID: 3dc553
Wall time: 0.1800 seconds
Process exited with code 0
Original token count: 22
Output:

   INFO  Test [tests/Feature/Storefront/OrderViewsTest.php] created successfully.


command: php artisan make:test --pest Storefront/OrderViewsTest --no-interaction

type: unknown

aggregated_output: INFO  Test [tests/Feature/Storefront/OrderViewsTest.php] created successfully.


command: php artisan make:livewire Storefront/Account/Orders/Index --class --no-interaction

aggregated_output: INFO  Livewire component [app/Livewire/Storefront/Account/Orders/Index.php] created successfully.


command: php artisan make:livewire Storefront/Checkout/Confirmation --class --no-interaction

aggregated_output: INFO  Livewire component [app/Livewire/Storefront/Checkout/Confirmation.php] created successfully.


command: php artisan make:livewire Storefront/Account/Orders/Show --class --no-interaction

aggregated_output: INFO  Livewire component [app/Livewire/Storefront/Account/Orders/Show.php] created successfully.


cmd: find app/Livewire/Storefront/Checkout app/Livewire/Storefront/Account/Orders resources/views/livewire/storefront/checkout resources/views/livewire/storefront/account/orders -type f -maxdepth 5 | sort


cmd: sed -n '1,120p' app/Livewire/Storefront/Checkout/Confirmation.php


cmd: sed -n '1,120p' app/Livewire/Storefront/Account/Orders/Index.php


cmd: sed -n '1,120p' app/Livewire/Storefront/Account/Orders/Show.php


command: sed -n '1,120p' app/Livewire/Storefront/Checkout/Confirmation.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Checkout;

use Livewire\Component;

class Confirmation extends Component
{
    public function render()
    {
        return view('livewire.storefront.checkout.confirmation');
    }
}


command: sed -n '1,120p' app/Livewire/Storefront/Account/Orders/Index.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Account\Orders;

use Livewire\Component;

class Index extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.orders.index');
    }
}


command: find app/Livewire/Storefront/Checkout app/Livewire/Storefront/Account/Orders resources/views/livewire/storefront/checkout resources/views/livewire/storefront/account/orders -type f -maxdepth 5 | sort

path: Checkout


command: sed -n '1,120p' app/Livewire/Storefront/Account/Orders/Show.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Account\Orders;

use Livewire\Component;

class Show extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.orders.show');
    }
}


output:
Chunk ID: b0aaa5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 113
Output:
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Livewire/Storefront/Checkout/Confirmation.php
app/Livewire/Storefront/Checkout/Show.php
resources/views/livewire/storefront/account/orders/index.blade.php
resources/views/livewire/storefront/account/orders/show.blade.php
resources/views/livewire/storefront/checkout/confirmation.blade.php
resources/views/livewire/storefront/checkout/show.blade.php


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

namespace App\Livewire\Storefront\Checkout;

use Livewire\Component;

class Confirmation extends Component
{
    public function render()
    {
        return view('livewire.storefront.checkout.confirmation');
    }
}


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

namespace App\Livewire\Storefront\Account\Orders;

use Livewire\Component;

class Index extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.orders.index');
    }
}


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

namespace App\Livewire\Storefront\Account\Orders;

use Livewire\Component;

class Show extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.orders.show');
    }
}


command:
/bin/zsh
-lc
sed -n '1,120p' app/Support/Money.php

Money.php

cmd: sed -n '1,120p' app/Support/Money.php

aggregated_output:
<?php

namespace App\Support;

class Money
{
    public static function format(int $amount, string $currency = 'EUR'): string
    {
        return number_format($amount / 100, 2, '.', ',').' '.$currency;
    }

    public static function fromDecimalString(string|int|float|null $amount): int
    {
        if ($amount === null || $amount === '') {
            return 0;
        }

        return (int) round(((float) $amount) * 100);
    }
}


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

namespace App\Support;

class Money
{
    public static function format(int $amount, string $currency = 'EUR'): string
    {
        return number_format($amount / 100, 2, '.', ',').' '.$currency;
    }

    public static function fromDecimalString(string|int|float|null $amount): int
    {
        if ($amount === null || $amount === '') {
            return 0;
        }

        return (int) round(((float) $amount) * 100);
    }
}


cmd: sed -n '1,260p' tests/Feature/Storefront/CartCheckoutUiTest.php


command: sed -n '1,260p' tests/Feature/Storefront/CartCheckoutUiTest.php

aggregated_output:
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Cart\Show as CartShow;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Livewire\Storefront\Products\Show as ProductShow;
use App\Models\Cart;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function storefrontUiStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

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

test('product detail add to cart persists a session cart', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);

    Livewire::test(ProductShow::class, ['handle' => 'classic-cotton-t-shirt'])
        ->set('quantity', 2)
        ->call('addToCart')
        ->assertDispatched('cart-updated');

    $cart = Cart::withoutGlobalScopes()->findOrFail(session('cart_id'));

    expect($cart->store_id)->toBe($store->getKey())
        ->and($cart->lines()->withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity)->toBe(2);
});

test('cart page updates and removes line quantities', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    $line = app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    Livewire::test(CartShow::class)
        ->assertSee('Classic Cotton T-Shirt')
        ->call('increaseQuantity', $line->getKey())
        ->assertDispatched('cart-updated');

    expect($line->refresh()->quantity)->toBe(3);

    Livewire::test(CartShow::class)
        ->call('removeLine', $line->getKey())
        ->assertDispatched('cart-updated');

    expect($cart->lines()->withoutGlobalScopes()->count())->toBe(0);
});

test('cart page applies discount estimates shipping and carries discount into checkout', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $component = Livewire::test(CartShow::class)
        ->set('discountCode', 'SAVE10')
        ->call('applyDiscount')
        ->assertHasNoErrors()
        ->assertSet('appliedDiscountCode', 'SAVE10')
        ->set('shippingCountry', 'DE')
        ->set('shippingPostalCode', '10115')
        ->call('estimateShipping')
        ->assertHasNoErrors()
        ->assertSee('Standard Shipping');

    expect(session('cart_discount_code'))->toBe('SAVE10')
        ->and($component->instance()->discountAmount())->toBe(500)
        ->and($component->instance()->estimatedShippingAmount())->toBe(799)
        ->and($component->instance()->estimatedTotal())->toBe(5297);

    Livewire::test(CheckoutShow::class)
        ->assertSet('discountCode', 'SAVE10');

    $checkout = Checkout::withoutGlobalScopes()->where('cart_id', $cart->getKey())->firstOrFail();

    expect($checkout->discount_code)->toBe('SAVE10')
        ->and($checkout->totals_json['discount'])->toBe(500);
});

test('customer login merges an existing guest cart without creating empty carts', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $customer = Customer::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('email', 'customer@acme.test')
        ->firstOrFail();
    $customerCart = app(CartService::class)->create($store, $customer);
    $guestCart = app(CartService::class)->create($store);
    session(['cart_id' => $guestCart->getKey()]);

    app(CartService::class)->addLine($customerCart, $variant->getKey(), 1);
    app(CartService::class)->addLine($guestCart, $variant->getKey(), 2);

    Livewire::test(CustomerLogin::class)
        ->set('email', 'customer@acme.test')
        ->set('password', 'password')
        ->call('login');

    expect($guestCart->refresh()->status)->toBe(CartStatus::Abandoned)
        ->and($customerCart->refresh()->lines()->withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity)->toBe(2)
        ->and(Cart::withoutGlobalScopes()->where('store_id', $store->getKey())->count())->toBe(2);
});

test('checkout page progresses through address shipping discount and payment selection', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $rate = ShippingRate::withoutGlobalScopes()
        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->where('name', 'Standard Shipping')
        ->firstOrFail();

    Livewire::test(CheckoutShow::class)
        ->set('email', 'buyer@example.test')
        ->set('shippingAddress.first_name', 'Test')
        ->set('shippingAddress.last_name', 'Buyer')
        ->set('shippingAddress.address1', 'Main Street 1')
        ->set('shippingAddress.city', 'Berlin')
        ->set('shippingAddress.country', 'DE')
        ->set('shippingAddress.postal_code', '10115')
        ->call('saveAddress')
        ->assertSet('step', 'shipping')
        ->set('selectedShippingRateId', $rate->getKey())
        ->call('selectShippingMethod')
        ->assertSet('step', 'payment')
        ->set('discountCode', 'SAVE10')
        ->call('applyDiscount')
        ->assertHasNoErrors()
        ->set('paymentMethod', 'credit_card')
        ->call('selectPaymentMethod')
        ->assertSet('step', 'reserved');

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

    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->discount_code)->toBe('SAVE10')
        ->and($checkout->totals_json['discount'])->toBeGreaterThan(0)
        ->and($inventory->quantity_reserved)->toBe(2);
});


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

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Cart\Show as CartShow;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Livewire\Storefront\Products\Show as ProductShow;
use App\Models\Cart;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function storefrontUiStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

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

test('product detail add to cart persists a session cart', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);

    Livewire::test(ProductShow::class, ['handle' => 'classic-cotton-t-shirt'])
        ->set('quantity', 2)
        ->call('addToCart')
        ->assertDispatched('cart-updated');

    $cart = Cart::withoutGlobalScopes()->findOrFail(session('cart_id'));

    expect($cart->store_id)->toBe($store->getKey())
        ->and($cart->lines()->withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity)->toBe(2);
});

test('cart page updates and removes line quantities', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    $line = app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    Livewire::test(CartShow::class)
        ->assertSee('Classic Cotton T-Shirt')
        ->call('increaseQuantity', $line->getKey())
        ->assertDispatched('cart-updated');

    expect($line->refresh()->quantity)->toBe(3);

    Livewire::test(CartShow::class)
        ->call('removeLine', $line->getKey())
        ->assertDispatched('cart-updated');

    expect($cart->lines()->withoutGlobalScopes()->count())->toBe(0);
});

test('cart page applies discount estimates shipping and carries discount into checkout', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $component = Livewire::test(CartShow::class)
        ->set('discountCode', 'SAVE10')
        ->call('applyDiscount')
        ->assertHasNoErrors()
        ->assertSet('appliedDiscountCode', 'SAVE10')
        ->set('shippingCountry', 'DE')
        ->set('shippingPostalCode', '10115')
        ->call('estimateShipping')
        ->assertHasNoErrors()
        ->assertSee('Standard Shipping');

    expect(session('cart_discount_code'))->toBe('SAVE10')
        ->and($component->instance()->discountAmount())->toBe(500)
        ->and($component->instance()->estimatedShippingAmount())->toBe(799)
        ->and($component->instance()->estimatedTotal())->toBe(5297);

    Livewire::test(CheckoutShow::class)
        ->assertSet('discountCode', 'SAVE10');

    $checkout = Checkout::withoutGlobalScopes()->where('cart_id', $cart->getKey())->firstOrFail();

    expect($checkout->discount_code)->toBe('SAVE10')
        ->and($checkout->totals_json['discount'])->toBe(500);
});

test('customer login merges an existing guest cart without creating empty carts', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $customer = Customer::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('email', 'customer@acme.test')
        ->firstOrFail();
    $customerCart = app(CartService::class)->create($store, $customer);
    $guestCart = app(CartService::class)->create($store);
    session(['cart_id' => $guestCart->getKey()]);

    app(CartService::class)->addLine($customerCart, $variant->getKey(), 1);
    app(CartService::class)->addLine($guestCart, $variant->getKey(), 2);

    Livewire::test(CustomerLogin::class)
        ->set('email', 'customer@acme.test')
        ->set('password', 'password')
        ->call('login');

    expect($guestCart->refresh()->status)->toBe(CartStatus::Abandoned)
        ->and($customerCart->refresh()->lines()->withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity)->toBe(2)
        ->and(Cart::withoutGlobalScopes()->where('store_id', $store->getKey())->count())->toBe(2);
});

test('checkout page progresses through address shipping discount and payment selection', function () {
    $store = storefrontUiStore();
    $variant = storefrontUiVariant($store);
    $cart = app(CartService::class)->create($store);
    session(['cart_id' => $cart->getKey()]);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $rate = ShippingRate::withoutGlobalScopes()
        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->where('name', 'Standard Shipping')
        ->firstOrFail();

    Livewire::test(CheckoutShow::class)
        ->set('email', 'buyer@example.test')
        ->set('shippingAddress.first_name', 'Test')
        ->set('shippingAddress.last_name', 'Buyer')
        ->set('shippingAddress.address1', 'Main Street 1')
        ->set('shippingAddress.city', 'Berlin')
        ->set('shippingAddress.country', 'DE')
        ->set('shippingAddress.postal_code', '10115')
        ->call('saveAddress')
        ->assertSet('step', 'shipping')
        ->set('selectedShippingRateId', $rate->getKey())
        ->call('selectShippingMethod')
        ->assertSet('step', 'payment')
        ->set('discountCode', 'SAVE10')
        ->call('applyDiscount')
        ->assertHasNoErrors()
        ->set('paymentMethod', 'credit_card')
        ->call('selectPaymentMethod')
        ->assertSet('step', 'reserved');

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

    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->discount_code)->toBe('SAVE10')
        ->and($checkout->totals_json['discount'])->toBeGreaterThan(0)
        ->and($inventory->quantity_reserved)->toBe(2);
});


message: I’m keeping the storefront order UI consistent with the existing class-based Livewire components and Flux cards/forms. The customer-facing routes will be `/checkout/confirmation/{order}`, `/account`, and `/account/orders/{order}`.

phase: commentary


type: message

role: assistant

text: I’m keeping the storefront order UI consistent with the existing class-based Livewire components and Flux cards/forms. The customer-facing routes will be `/checkout/confirmation/{order}`, `/account`, and `/account/orders/{order}`.


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/routes/web.php
@@
 use App\Livewire\Admin\Inventory\Index as AdminInventoryIndex;
 use App\Livewire\Admin\Products\Form as AdminProductForm;
 use App\Livewire\Admin\Products\Index as AdminProductsIndex;
+use App\Livewire\Storefront\Account\Orders\Index as CustomerOrdersIndex;
+use App\Livewire\Storefront\Account\Orders\Show as CustomerOrderShow;
 use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
 use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
 use App\Livewire\Storefront\Cart\Show as StorefrontCartShow;
+use App\Livewire\Storefront\Checkout\Confirmation as StorefrontCheckoutConfirmation;
 use App\Livewire\Storefront\Checkout\Show as StorefrontCheckoutShow;
@@
     Route::livewire('cart', StorefrontCartShow::class)->name('cart.show');
     Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
+    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
     Route::livewire('search', StorefrontSearchIndex::class)->name('search.index');
     Route::livewire('pages/{handle}', StorefrontPageShow::class)->name('pages.show');
 });
@@
-    Route::view('account', 'storefront.account.dashboard')
+    Route::livewire('account', CustomerOrdersIndex::class)
         ->middleware('auth:customer')
         ->name('account.dashboard');
+
+    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
+        ->middleware('auth:customer')
+        ->name('account.orders.show');
 });
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Show.php
@@
 use App\Enums\CheckoutStatus;
 use App\Exceptions\InvalidCheckoutTransitionException;
 use App\Exceptions\InvalidDiscountException;
+use App\Exceptions\PaymentFailedException;
 use App\Exceptions\UnserviceableShippingAddressException;
@@
     public string $paymentMethod = 'credit_card';
+
+    public string $cardNumber = '';
+
+    public string $cardName = '';
+
+    public string $cardExpiry = '';
+
+    public string $cardCvc = '';
@@
     public function selectPaymentMethod(): void
@@
         }
     }
+
+    public function placeOrder(): void
+    {
+        $rules = [
+            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
+        ];
+
+        if ($this->paymentMethod === 'credit_card') {
+            $rules = [
+                ...$rules,
+                'cardNumber' => ['required', 'string'],
+                'cardName' => ['nullable', 'string'],
+                'cardExpiry' => ['nullable', 'string'],
+                'cardCvc' => ['nullable', 'string'],
+            ];
+        }
+
+        $this->validate($rules);
+
+        $checkout = $this->checkout();
+
+        if (! $checkout instanceof Checkout) {
+            return;
+        }
+
+        try {
+            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
+                $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
+            }
+
+            $order = app(CheckoutService::class)->completeCheckout($checkout, [
+                'card_number' => $this->cardNumber,
+                'cardholder_name' => $this->cardName,
+                'expiry' => $this->cardExpiry,
+                'cvc' => $this->cardCvc,
+            ]);
+
+            session([
+                'last_order_id' => $order->getKey(),
+            ]);
+            session()->forget(['cart_id', 'cart_discount_code']);
+
+            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
+        } catch (InvalidCheckoutTransitionException|PaymentFailedException $exception) {
+            $this->step = 'payment';
+
+            throw ValidationException::withMessages([
+                $this->paymentMethod === 'credit_card' ? 'cardNumber' : 'paymentMethod' => $exception->getMessage(),
+            ]);
+        }
+    }
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Confirmation.php
@@
 namespace App\Livewire\Storefront\Checkout;

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;

 class Confirmation extends Component
 {
-    public function render()
+    public int $orderId;
+
+    public function mount(Order $order): void
     {
-        return view('livewire.storefront.checkout.confirmation');
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $order = Order::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereKey($order->getKey())
+            ->firstOrFail();
+        $customer = Auth::guard('customer')->user();
+        $sessionOrderId = session('last_order_id');
+        $isCustomerOrder = $customer instanceof Customer && $order->customer_id === $customer->getKey();
+        $isSessionOrder = $sessionOrderId !== null && (int) $sessionOrderId === (int) $order->getKey();
+
+        abort_unless($isCustomerOrder || $isSessionOrder, 404);
+
+        $this->orderId = $order->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.checkout.confirmation', [
+            'order' => $this->order(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Order confirmation',
+        ]);
+    }
+
+    private function order(): Order
+    {
+        return Order::withoutGlobalScopes()
+            ->with(['lines', 'payments', 'fulfillments.lines'])
+            ->findOrFail($this->orderId);
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Index.php
@@
 namespace App\Livewire\Storefront\Account\Orders;

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;

 class Index extends Component
 {
-    public function render()
+    public int $storeId;
+
+    public function mount(): void
     {
-        return view('livewire.storefront.account.orders.index');
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $this->storeId = $store->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.account.orders.index', [
+            'orders' => $this->orders(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Account',
+        ]);
+    }
+
+    /**
+     * @return Collection<int, Order>
+     */
+    private function orders(): Collection
+    {
+        $customer = Auth::guard('customer')->user();
+
+        abort_unless($customer instanceof Customer, 403);
+
+        return Order::withoutGlobalScopes()
+            ->where('store_id', $this->storeId)
+            ->where('customer_id', $customer->getKey())
+            ->latest('placed_at')
+            ->limit(20)
+            ->get();
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Show.php
@@
 namespace App\Livewire\Storefront\Account\Orders;

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;

 class Show extends Component
 {
-    public function render()
+    public int $orderId;
+
+    public function mount(Order $order): void
     {
-        return view('livewire.storefront.account.orders.show');
+        $store = app('current_store');
+        $customer = Auth::guard('customer')->user();
+
+        abort_unless($store instanceof Store && $customer instanceof Customer, 404);
+
+        $order = Order::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->where('customer_id', $customer->getKey())
+            ->whereKey($order->getKey())
+            ->firstOrFail();
+
+        $this->orderId = $order->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.account.orders.show', [
+            'order' => $this->order(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Order details',
+        ]);
+    }
+
+    private function order(): Order
+    {
+        return Order::withoutGlobalScopes()
+            ->with(['lines', 'payments', 'refunds', 'fulfillments.lines'])
+            ->findOrFail($this->orderId);
     }
 }
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/routes/web.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Show.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Confirmation.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Index.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Show.php

type: update

unified_diff:
@@ -8,2 +8,4 @@
 use App\Livewire\Admin\Products\Index as AdminProductsIndex;
+use App\Livewire\Storefront\Account\Orders\Index as CustomerOrdersIndex;
+use App\Livewire\Storefront\Account\Orders\Show as CustomerOrderShow;
 use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
@@ -11,2 +13,3 @@
 use App\Livewire\Storefront\Cart\Show as StorefrontCartShow;
+use App\Livewire\Storefront\Checkout\Confirmation as StorefrontCheckoutConfirmation;
 use App\Livewire\Storefront\Checkout\Show as StorefrontCheckoutShow;
@@ -28,2 +31,3 @@
     Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
+    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
     Route::livewire('search', StorefrontSearchIndex::class)->name('search.index');
@@ -65,5 +69,9 @@

-    Route::view('account', 'storefront.account.dashboard')
+    Route::livewire('account', CustomerOrdersIndex::class)
         ->middleware('auth:customer')
         ->name('account.dashboard');
+
+    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
+        ->middleware('auth:customer')
+        ->name('account.orders.show');
 });

unified_diff:
@@ -4,2 +4,6 @@

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;
@@ -8,5 +12,38 @@
 {
-    public function render()
+    public int $orderId;
+
+    public function mount(Order $order): void
     {
-        return view('livewire.storefront.checkout.confirmation');
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $order = Order::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereKey($order->getKey())
+            ->firstOrFail();
+        $customer = Auth::guard('customer')->user();
+        $sessionOrderId = session('last_order_id');
+        $isCustomerOrder = $customer instanceof Customer && $order->customer_id === $customer->getKey();
+        $isSessionOrder = $sessionOrderId !== null && (int) $sessionOrderId === (int) $order->getKey();
+
+        abort_unless($isCustomerOrder || $isSessionOrder, 404);
+
+        $this->orderId = $order->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.checkout.confirmation', [
+            'order' => $this->order(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Order confirmation',
+        ]);
+    }
+
+    private function order(): Order
+    {
+        return Order::withoutGlobalScopes()
+            ->with(['lines', 'payments', 'fulfillments.lines'])
+            ->findOrFail($this->orderId);
     }

unified_diff:
@@ -4,2 +4,6 @@

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;
@@ -8,5 +12,34 @@
 {
-    public function render()
+    public int $orderId;
+
+    public function mount(Order $order): void
     {
-        return view('livewire.storefront.account.orders.show');
+        $store = app('current_store');
+        $customer = Auth::guard('customer')->user();
+
+        abort_unless($store instanceof Store && $customer instanceof Customer, 404);
+
+        $order = Order::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->where('customer_id', $customer->getKey())
+            ->whereKey($order->getKey())
+            ->firstOrFail();
+
+        $this->orderId = $order->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.account.orders.show', [
+            'order' => $this->order(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Order details',
+        ]);
+    }
+
+    private function order(): Order
+    {
+        return Order::withoutGlobalScopes()
+            ->with(['lines', 'payments', 'refunds', 'fulfillments.lines'])
+            ->findOrFail($this->orderId);
     }

unified_diff:
@@ -7,2 +7,3 @@
 use App\Exceptions\InvalidDiscountException;
+use App\Exceptions\PaymentFailedException;
 use App\Exceptions\UnserviceableShippingAddressException;
@@ -67,2 +68,10 @@

+    public string $cardNumber = '';
+
+    public string $cardName = '';
+
+    public string $cardExpiry = '';
+
+    public string $cardCvc = '';
+
     public function mount(): void
@@ -190,2 +199,53 @@

+    public function placeOrder(): void
+    {
+        $rules = [
+            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
+        ];
+
+        if ($this->paymentMethod === 'credit_card') {
+            $rules = [
+                ...$rules,
+                'cardNumber' => ['required', 'string'],
+                'cardName' => ['nullable', 'string'],
+                'cardExpiry' => ['nullable', 'string'],
+                'cardCvc' => ['nullable', 'string'],
+            ];
+        }
+
+        $this->validate($rules);
+
+        $checkout = $this->checkout();
+
+        if (! $checkout instanceof Checkout) {
+            return;
+        }
+
+        try {
+            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
+                $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
+            }
+
+            $order = app(CheckoutService::class)->completeCheckout($checkout, [
+                'card_number' => $this->cardNumber,
+                'cardholder_name' => $this->cardName,
+                'expiry' => $this->cardExpiry,
+                'cvc' => $this->cardCvc,
+            ]);
+
+            session([
+                'last_order_id' => $order->getKey(),
+            ]);
+            session()->forget(['cart_id', 'cart_discount_code']);
+
+            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
+        } catch (InvalidCheckoutTransitionException|PaymentFailedException $exception) {
+            $this->step = 'payment';
+
+            throw ValidationException::withMessages([
+                $this->paymentMethod === 'credit_card' ? 'cardNumber' : 'paymentMethod' => $exception->getMessage(),
+            ]);
+        }
+    }
+
     public function store(): Store

unified_diff:
@@ -4,2 +4,7 @@

+use App\Models\Customer;
+use App\Models\Order;
+use App\Models\Store;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
 use Livewire\Component;
@@ -8,5 +13,37 @@
 {
-    public function render()
+    public int $storeId;
+
+    public function mount(): void
     {
-        return view('livewire.storefront.account.orders.index');
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $this->storeId = $store->getKey();
+    }
+
+    public function render(): mixed
+    {
+        return view('livewire.storefront.account.orders.index', [
+            'orders' => $this->orders(),
+        ])->layout('layouts.storefront', [
+            'title' => 'Account',
+        ]);
+    }
+
+    /**
+     * @return Collection<int, Order>
+     */
+    private function orders(): Collection
+    {
+        $customer = Auth::guard('customer')->user();
+
+        abort_unless($customer instanceof Customer, 403);
+
+        return Order::withoutGlobalScopes()
+            ->where('store_id', $this->storeId)
+            ->where('customer_id', $customer->getKey())
+            ->latest('placed_at')
+            ->limit(20)
+            ->get();
     }


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/routes/web.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Show.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Confirmation.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Index.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Orders/Show.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/show.blade.php
@@
                             <flux:select wire:model="paymentMethod" label="Payment method">
                                 <flux:select.option value="credit_card">Credit card</flux:select.option>
                                 <flux:select.option value="paypal">PayPal</flux:select.option>
                                 <flux:select.option value="bank_transfer">Bank transfer</flux:select.option>
                             </flux:select>
                             <flux:error name="paymentMethod" />

-                            <flux:button wire:click="selectPaymentMethod" variant="primary" class="w-full">
-                                Reserve items
-                            </flux:button>
+                            @if ($paymentMethod === 'credit_card')
+

+

+                                        <flux:input wire:model="cardNumber" label="Card number" placeholder="4242 4242 4242 4242" />
+                                        <flux:error name="cardNumber" />
+

+                                    <flux:input wire:model="cardName" label="Name on card" />
+                                    <flux:input wire:model="cardExpiry" label="Expiry" placeholder="12/30" />
+                                    <flux:input wire:model="cardCvc" label="CVC" />
+

+                            @endif
+
+                            @if ($step === 'reserved')
+                                <flux:button wire:click="placeOrder" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Place order
+                                </flux:button>
+                            @else
+                                <flux:button wire:click="selectPaymentMethod" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Reserve items
+                                </flux:button>
+                            @endif

                     @endif

*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/confirmation.blade.php
@@
-

-    {{-- A good traveler has no fixed plans and is not intent upon arriving. --}}
-

+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Cart', 'url' => route('cart.show')],
+        ['label' => 'Checkout', 'url' => route('checkout.show')],
+        ['label' => 'Confirmation'],
+    ]" />
+
+

+

+

+                <flux:badge color="green">Order placed</flux:badge>
+

+                    {{ $order->order_number }}
+

+

+                    Confirmation sent to {{ $order->email }}.
+

+

+
+            @if ($order->payment_method === \App\Enums\PaymentMethod::BankTransfer)
+

+                    <flux:heading size="lg">Bank transfer</flux:heading>
+

+

+
Bank

+
Mock Bank AG

+

+

+
BIC

+
COBADEFFXXX

+

+

+
IBAN

+
DE89 3704 0044 0532 0130 00

+

+

+
Reference

+
{{ $order->order_number }}

+

+

+
Amount

+
{{ \App\Support\Money::format($order->total_amount, $order->currency) }}

+

+

+

+            @endif
+
+

+                <flux:heading size="lg">Items</flux:heading>
+

+                    @foreach ($order->lines as $line)
+
getKey() }}">
+

+
{{ $line->title_snapshot }}

+
Qty {{ $line->quantity }}

+

+                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />
+

+                    @endforeach
+

+

+

+
+
+
Summary

+

+

+                    Subtotal
+                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />
+

+

+                    Discount
+                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}
+

+

+                    Shipping
+                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />
+

+

+                    Tax
+                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />
+

+

+                    Total
+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />
+

+

+
+            <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6 w-full">
+                Continue shopping
+            </flux:button>
+
+

+

*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/index.blade.php
@@
-

-    {{-- A good traveler has no fixed plans and is not intent upon arriving. --}}
-

+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Account'],
+    ]" />
+
+

+
Orders

+
+

+            @forelse ($orders as $order)
+
+

+
{{ $order->order_number }}

+
{{ $order->placed_at?->format('M j, Y') }}

+

+

+                        <flux:badge>{{ $order->financial_status->value }}</flux:badge>
+                        <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>
+

+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" class="justify-start sm:justify-end" />
+
+            @empty
+

+                    <flux:icon name="shopping-bag" class="mx-auto size-10 text-zinc-400 dark:text-zinc-600" />
+
No orders yet

+                    <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6">
+                        Browse products
+                    </flux:button>
+

+            @endforelse
+

+

+

*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/show.blade.php
@@
-

-    {{-- A good traveler has no fixed plans and is not intent upon arriving. --}}
-

+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Account', 'url' => route('account.dashboard')],
+        ['label' => $order->order_number],
+    ]" />
+
+

+

+

+
{{ $order->order_number }}

+
{{ $order->placed_at?->format('M j, Y') }}

+

+
+

+                <flux:heading size="lg">Items</flux:heading>
+

+                    @foreach ($order->lines as $line)
+
getKey() }}">
+

+
{{ $line->title_snapshot }}

+
Qty {{ $line->quantity }}

+

+                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />
+

+                    @endforeach
+

+

+
+            @if ($order->fulfillments->isNotEmpty())
+

+                    <flux:heading size="lg">Fulfillment</flux:heading>
+

+                        @foreach ($order->fulfillments as $fulfillment)
+
getKey() }}">
+

+                                    {{ $fulfillment->status->value }}
+                                    @if ($fulfillment->tracking_number)
+                                        {{ $fulfillment->tracking_number }}
+                                    @endif
+

+

+                        @endforeach
+

+

+            @endif
+

+
+
+
Summary

+

+                <flux:badge>{{ $order->financial_status->value }}</flux:badge>
+                <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>
+

+

+

+                    Subtotal
+                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />
+

+

+                    Discount
+                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}
+

+

+                    Shipping
+                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />
+

+

+                    Tax
+                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />
+

+

+                    Total
+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />
+

+

+
+

+

*** End Patch


output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/confirmation.blade.php:
<div>
    {{-- A good traveler has no fixed plans and is not intent upon arriving. --}}
</div>


cmd: sed -n '1,40p' resources/views/livewire/storefront/checkout/confirmation.blade.php


cmd: sed -n '1,40p' resources/views/livewire/storefront/account/orders/index.blade.php


cmd: sed -n '1,40p' resources/views/livewire/storefront/account/orders/show.blade.php


cmd: rg -n "Reserve items|Payment method|cardNumber" resources/views/livewire/storefront/checkout/show.blade.php


command: sed -n '1,40p' resources/views/livewire/storefront/account/orders/show.blade.php

aggregated_output: {{-- Simplicity is the ultimate sophistication. - Leonardo da Vinci --}}


command: sed -n '1,40p' resources/views/livewire/storefront/checkout/confirmation.blade.php

aggregated_output: {{-- Act only according to that maxim whereby you can, at the same time, will that it should become a universal law. - Immanuel Kant --}}


command: sed -n '1,40p' resources/views/livewire/storefront/account/orders/index.blade.php

aggregated_output: {{-- Knowing is not enough; we must apply. Being willing is not enough; we must do. - Leonardo da Vinci --}}


output:
Chunk ID: 7a029a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 39
Output:
<div>
    {{-- Act only according to that maxim whereby you can, at the same time, will that it should become a universal law. - Immanuel Kant --}}
</div>


output:
Chunk ID: e1f62e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 32
Output:
<div>
    {{-- Knowing is not enough; we must apply. Being willing is not enough; we must do. - Leonardo da Vinci --}}
</div>


output:
Chunk ID: bf66a7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 23
Output:
<div>
    {{-- Simplicity is the ultimate sophistication. - Leonardo da Vinci --}}
</div>


command: rg -n "Reserve items|Payment method|cardNumber" resources/views/livewire/storefront/checkout/show.blade.php

type: search

cmd: rg -n 'Reserve items|Payment method|cardNumber' resources/views/livewire/storefront/checkout/show.blade.php

query: Reserve items|Payment method|cardNumber

aggregated_output:
160:                            <flux:select wire:model="paymentMethod" label="Payment method">
168:                                Reserve items


output:
Chunk ID: 6d1e7e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 37
Output:
160:                            <flux:select wire:model="paymentMethod" label="Payment method">
168:                                Reserve items


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/show.blade.php
@@
                             <flux:select wire:model="paymentMethod" label="Payment method">
                                 <flux:select.option value="credit_card">Credit card</flux:select.option>
                                 <flux:select.option value="paypal">PayPal</flux:select.option>
                                 <flux:select.option value="bank_transfer">Bank transfer</flux:select.option>
                             </flux:select>
                             <flux:error name="paymentMethod" />

-                            <flux:button wire:click="selectPaymentMethod" variant="primary" class="w-full">
-                                Reserve items
-                            </flux:button>
+                            @if ($paymentMethod === 'credit_card')
+

+

+                                        <flux:input wire:model="cardNumber" label="Card number" placeholder="4242 4242 4242 4242" />
+                                        <flux:error name="cardNumber" />
+

+                                    <flux:input wire:model="cardName" label="Name on card" />
+                                    <flux:input wire:model="cardExpiry" label="Expiry" placeholder="12/30" />
+                                    <flux:input wire:model="cardCvc" label="CVC" />
+

+                            @endif
+
+                            @if ($step === 'reserved')
+                                <flux:button wire:click="placeOrder" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Place order
+                                </flux:button>
+                            @else
+                                <flux:button wire:click="selectPaymentMethod" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Reserve items
+                                </flux:button>
+                            @endif

                     @endif

*** Delete File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/confirmation.blade.php
*** Add File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/checkout/confirmation.blade.php
+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Cart', 'url' => route('cart.show')],
+        ['label' => 'Checkout', 'url' => route('checkout.show')],
+        ['label' => 'Confirmation'],
+    ]" />
+
+

+

+

+                <flux:badge color="green">Order placed</flux:badge>
+

+                    {{ $order->order_number }}
+

+

+                    Confirmation sent to {{ $order->email }}.
+

+

+
+            @if ($order->payment_method === \App\Enums\PaymentMethod::BankTransfer)
+

+                    <flux:heading size="lg">Bank transfer</flux:heading>
+

+

+
Bank

+
Mock Bank AG

+

+

+
BIC

+
COBADEFFXXX

+

+

+
IBAN

+
DE89 3704 0044 0532 0130 00

+

+

+
Reference

+
{{ $order->order_number }}

+

+

+
Amount

+
{{ \App\Support\Money::format($order->total_amount, $order->currency) }}

+

+

+

+            @endif
+
+

+                <flux:heading size="lg">Items</flux:heading>
+

+                    @foreach ($order->lines as $line)
+
getKey() }}">
+

+
{{ $line->title_snapshot }}

+
Qty {{ $line->quantity }}

+

+                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />
+

+                    @endforeach
+

+

+

+
+
+
Summary

+

+

+                    Subtotal
+                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />
+

+

+                    Discount
+                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}
+

+

+                    Shipping
+                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />
+

+

+                    Tax
+                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />
+

+

+                    Total
+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />
+

+

+
+            <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6 w-full">
+                Continue shopping
+            </flux:button>
+
+

+

*** Delete File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/index.blade.php
*** Add File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/index.blade.php
+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Account'],
+    ]" />
+
+

+
Orders

+
+

+            @forelse ($orders as $order)
+
+

+
{{ $order->order_number }}

+
{{ $order->placed_at?->format('M j, Y') }}

+

+

+                        <flux:badge>{{ $order->financial_status->value }}</flux:badge>
+                        <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>
+

+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" class="justify-start sm:justify-end" />
+
+            @empty
+

+                    <flux:icon name="shopping-bag" class="mx-auto size-10 text-zinc-400 dark:text-zinc-600" />
+
No orders yet

+                    <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6">
+                        Browse products
+                    </flux:button>
+

+            @endforelse
+

+

+

*** Delete File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/show.blade.php
*** Add File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/account/orders/show.blade.php
+

+    <x-storefront.breadcrumbs :items="[
+        ['label' => 'Account', 'url' => route('account.dashboard')],
+        ['label' => $order->order_number],
+    ]" />
+
+

+

+

+
{{ $order->order_number }}

+
{{ $order->placed_at?->format('M j, Y') }}

+

+
+

+                <flux:heading size="lg">Items</flux:heading>
+

+                    @foreach ($order->lines as $line)
+
getKey() }}">
+

+
{{ $line->title_snapshot }}

+
Qty {{ $line->quantity }}

+

+                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />
+

+                    @endforeach
+

+

+
+            @if ($order->fulfillments->isNotEmpty())
+

+                    <flux:heading size="lg">Fulfillment</flux:heading>
+

+                        @foreach ($order->fulfillments as $fulfillment)
+
getKey() }}">
+

+                                    {{ $fulfillment->status->value }}
+                                    @if ($fulfillment->tracking_number)
+                                        {{ $fulfillment->tracking_number }}
+                                    @endif
+

+

+                        @endforeach
+

+

+            @endif
+

+
+
+
Summary

+

+                <flux:badge>{{ $order->financial_status->value }}</flux:badge>
+                <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>
+

+

+

+                    Subtotal
+                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />
+

+

+                    Discount
+                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}
+

+

+                    Shipping
+                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />
+

+

+                    Tax
+                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />
+

+

+                    Total
+                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />
+

+

+
+

+

*** End Patch


unified_diff:
@@ -166,5 +166,23 @@

-                            <flux:button wire:click="selectPaymentMethod" variant="primary" class="w-full">
-                                Reserve items
-                            </flux:button>
+                            @if ($paymentMethod === 'credit_card')
+

+

+                                        <flux:input wire:model="cardNumber" label="Card number" placeholder="4242 4242 4242 4242" />
+                                        <flux:error name="cardNumber" />
+

+                                    <flux:input wire:model="cardName" label="Name on card" />
+                                    <flux:input wire:model="cardExpiry" label="Expiry" placeholder="12/30" />
+                                    <flux:input wire:model="cardCvc" label="CVC" />
+

+                            @endif
+
+                            @if ($step === 'reserved')
+                                <flux:button wire:click="placeOrder" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Place order
+                                </flux:button>
+                            @else
+                                <flux:button wire:click="selectPaymentMethod" wire:loading.attr="disabled" variant="primary" class="w-full">
+                                    Reserve items
+                                </flux:button>
+                            @endif

type: add

content:
    <x-storefront.breadcrumbs :items="[
        ['label' => 'Account'],
    ]" />

Orders

            @forelse ($orders as $order)

{{ $order->order_number }}

{{ $order->placed_at?->format('M j, Y') }}

                        <flux:badge>{{ $order->financial_status->value }}</flux:badge>
                        <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>

                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" class="justify-start sm:justify-end" />

            @empty

                    <flux:icon name="shopping-bag" class="mx-auto size-10 text-zinc-400 dark:text-zinc-600" />

No orders yet

                    <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6">
                        Browse products
                    </flux:button>

            @endforelse

content:
    <x-storefront.breadcrumbs :items="[
        ['label' => 'Account', 'url' => route('account.dashboard')],
        ['label' => $order->order_number],
    ]" />

{{ $order->order_number }}

{{ $order->placed_at?->format('M j, Y') }}

                <flux:heading size="lg">Items</flux:heading>

                    @foreach ($order->lines as $line)

getKey() }}">

{{ $line->title_snapshot }}

Qty {{ $line->quantity }}

                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />

                    @endforeach

            @if ($order->fulfillments->isNotEmpty())

                    <flux:heading size="lg">Fulfillment</flux:heading>

                        @foreach ($order->fulfillments as $fulfillment)

getKey() }}">

                                    {{ $fulfillment->status->value }}
                                    @if ($fulfillment->tracking_number)
                                        {{ $fulfillment->tracking_number }}
                                    @endif

                        @endforeach

            @endif

Summary

                <flux:badge>{{ $order->financial_status->value }}</flux:badge>
                <flux:badge>{{ $order->fulfillment_status->value }}</flux:badge>

                    Subtotal
                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />

                    Discount
                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}

                    Shipping
                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />

                    Tax
                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />

                    Total
                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />

content:
    <x-storefront.breadcrumbs :items="[
        ['label' => 'Cart', 'url' => route('cart.show')],
        ['label' => 'Checkout', 'url' => route('checkout.show')],
        ['label' => 'Confirmation'],
    ]" />

                <flux:badge color="green">Order placed</flux:badge>

                    {{ $order->order_number }}

                    Confirmation sent to {{ $order->email }}.

            @if ($order->payment_method === \App\Enums\PaymentMethod::BankTransfer)

                    <flux:heading size="lg">Bank transfer</flux:heading>

Bank

Mock Bank AG

BIC

COBADEFFXXX

IBAN

DE89 3704 0044 0532 0130 00

Reference

{{ $order->order_number }}

Amount

{{ \App\Support\Money::format($order->total_amount, $order->currency) }}

            @endif

                <flux:heading size="lg">Items</flux:heading>

                    @foreach ($order->lines as $line)

getKey() }}">

{{ $line->title_snapshot }}

Qty {{ $line->quantity }}

                            <x-storefront.price :amount="$line->total_amount" :currency="$order->currency" />

                    @endforeach

Summary

                    Subtotal
                    <x-storefront.price :amount="$order->subtotal_amount" :currency="$order->currency" />

                    Discount
                    -{{ \App\Support\Money::format($order->discount_amount, $order->currency) }}

                    Shipping
                    <x-storefront.price :amount="$order->shipping_amount" :currency="$order->currency" />

                    Tax
                    <x-storefront.price :amount="$order->tax_amount" :currency="$order->currency" />

                    Total
                    <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" />

            <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-6 w-full">
                Continue shopping
            </flux:button>


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Storefront/OrderViewsTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
-
-    $response->assertStatus(200);
-});
+use App\Enums\CartStatus;
+use App\Enums\CheckoutStatus;
+use App\Livewire\Storefront\Account\Orders\Index as AccountOrdersIndex;
+use App\Livewire\Storefront\Account\Orders\Show as AccountOrderShow;
+use App\Livewire\Storefront\Checkout\Confirmation;
+use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
+use App\Models\Cart;
+use App\Models\Checkout;
+use App\Models\Customer;
+use App\Models\InventoryItem;
+use App\Models\Order;
+use App\Models\OrderLine;
+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\ShippingRate;
+use App\Models\Store;
+use App\Services\CartService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Livewire\Livewire;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function () {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function orderViewsStore(): Store
+{
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    app()->instance('current_store', $store);
+
+    return $store;
+}
+
+function orderViewsVariant(Store $store): ProductVariant
+{
+    $product = Product::withoutGlobalScopes()
+        ->where('store_id', $store->getKey())
+        ->where('handle', 'classic-cotton-t-shirt')
+        ->firstOrFail();
+
+    return ProductVariant::withoutGlobalScopes()
+        ->where('product_id', $product->getKey())
+        ->oldest('position')
+        ->firstOrFail();
+}
+
+function orderViewsShippingRate(Store $store): ShippingRate
+{
+    return ShippingRate::withoutGlobalScopes()
+        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
+        ->where('name', 'Standard Shipping')
+        ->firstOrFail();
+}
+
+test('checkout page places an order and redirects to confirmation', function () {
+    $store = orderViewsStore();
+    $variant = orderViewsVariant($store);
+    $cart = app(CartService::class)->create($store);
+    session(['cart_id' => $cart->getKey()]);
+    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
+
+    $component = Livewire::test(CheckoutShow::class)
+        ->set('email', 'buyer@example.test')
+        ->set('shippingAddress.first_name', 'Test')
+        ->set('shippingAddress.last_name', 'Buyer')
+        ->set('shippingAddress.address1', 'Main Street 1')
+        ->set('shippingAddress.city', 'Berlin')
+        ->set('shippingAddress.country', 'DE')
+        ->set('shippingAddress.postal_code', '10115')
+        ->call('saveAddress')
+        ->set('selectedShippingRateId', orderViewsShippingRate($store)->getKey())
+        ->call('selectShippingMethod')
+        ->set('paymentMethod', 'credit_card')
+        ->call('selectPaymentMethod')
+        ->set('cardNumber', '4242 4242 4242 4242')
+        ->call('placeOrder');
+
+    $order = Order::withoutGlobalScopes()->firstOrFail();
+
+    $component->assertRedirect(route('checkout.confirmation', $order));
+
+    expect($order->email)->toBe('buyer@example.test')
+        ->and($order->lines)->toHaveCount(1)
+        ->and($cart->refresh()->status)->toBe(CartStatus::Converted)
+        ->and(session('last_order_id'))->toBe($order->getKey())
+        ->and(session('cart_id'))->toBeNull();
+});
+
+test('checkout page surfaces payment failures and releases reservations', function () {
+    $store = orderViewsStore();
+    $variant = orderViewsVariant($store);
+    $cart = app(CartService::class)->create($store);
+    session(['cart_id' => $cart->getKey()]);
+    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
+
+    Livewire::test(CheckoutShow::class)
+        ->set('email', 'buyer@example.test')
+        ->set('shippingAddress.first_name', 'Test')
+        ->set('shippingAddress.last_name', 'Buyer')
+        ->set('shippingAddress.address1', 'Main Street 1')
+        ->set('shippingAddress.city', 'Berlin')
+        ->set('shippingAddress.country', 'DE')
+        ->set('shippingAddress.postal_code', '10115')
+        ->call('saveAddress')
+        ->set('selectedShippingRateId', orderViewsShippingRate($store)->getKey())
+        ->call('selectShippingMethod')
+        ->set('paymentMethod', 'credit_card')
+        ->call('selectPaymentMethod')
+        ->set('cardNumber', '4000 0000 0000 0002')
+        ->call('placeOrder')
+        ->assertHasErrors('cardNumber')
+        ->assertSet('step', 'payment');
+
+    $checkout = Checkout::withoutGlobalScopes()->where('cart_id', $cart->getKey())->firstOrFail();
+    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
+
+    expect(Order::withoutGlobalScopes()->count())->toBe(0)
+        ->and($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
+        ->and($inventory->quantity_reserved)->toBe(0);
+});
+
+test('order confirmation is visible only for the session order or customer order', function () {
+    $store = orderViewsStore();
+    $customer = Customer::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
+    $order = Order::factory()->forCustomer($customer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $customer->getKey(),
+        'email' => $customer->email,
+    ]);
+
+    session(['last_order_id' => $order->getKey()]);
+
+    Livewire::test(Confirmation::class, ['order' => $order])
+        ->assertSee($order->order_number);
+
+    session()->forget('last_order_id');
+
+    Livewire::test(Confirmation::class, ['order' => $order])
+        ->assertStatus(404);
+});
+
+test('customer account lists and shows customer orders', function () {
+    $store = orderViewsStore();
+    $customer = Customer::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
+    $otherCustomer = Customer::factory()->create(['store_id' => $store->getKey()]);
+    $order = Order::factory()->forCustomer($customer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $customer->getKey(),
+        'order_number' => '#7777',
+    ]);
+    $otherOrder = Order::factory()->forCustomer($otherCustomer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $otherCustomer->getKey(),
+        'order_number' => '#8888',
+    ]);
+    OrderLine::factory()->create([
+        'order_id' => $order->getKey(),
+        'title_snapshot' => 'Classic Cotton T-Shirt',
+    ]);
+
+    $this->actingAs($customer, 'customer');
+
+    Livewire::test(AccountOrdersIndex::class)
+        ->assertSee('#7777')
+        ->assertDontSee('#8888');
+
+    Livewire::test(AccountOrderShow::class, ['order' => $order])
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('#7777');
+
+    Livewire::test(AccountOrderShow::class, ['order' => $otherOrder])
+        ->assertStatus(404);
+});
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Feature/Storefront/OrderViewsTest.php

unified_diff:
@@ -2,6 +2,177 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Enums\CartStatus;
+use App\Enums\CheckoutStatus;
+use App\Livewire\Storefront\Account\Orders\Index as AccountOrdersIndex;
+use App\Livewire\Storefront\Account\Orders\Show as AccountOrderShow;
+use App\Livewire\Storefront\Checkout\Confirmation;
+use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
+use App\Models\Cart;
+use App\Models\Checkout;
+use App\Models\Customer;
+use App\Models\InventoryItem;
+use App\Models\Order;
+use App\Models\OrderLine;
+use App\Models\Product;
+use App\Models\ProductVariant;
+use App\Models\ShippingRate;
+use App\Models\Store;
+use App\Services\CartService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Livewire\Livewire;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function () {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function orderViewsStore(): Store
+{
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    app()->instance('current_store', $store);
+
+    return $store;
+}
+
+function orderViewsVariant(Store $store): ProductVariant
+{
+    $product = Product::withoutGlobalScopes()
+        ->where('store_id', $store->getKey())
+        ->where('handle', 'classic-cotton-t-shirt')
+        ->firstOrFail();

-    $response->assertStatus(200);
+    return ProductVariant::withoutGlobalScopes()
+        ->where('product_id', $product->getKey())
+        ->oldest('position')
+        ->firstOrFail();
+}
+
+function orderViewsShippingRate(Store $store): ShippingRate
+{
+    return ShippingRate::withoutGlobalScopes()
+        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
+        ->where('name', 'Standard Shipping')
+        ->firstOrFail();
+}
+
+test('checkout page places an order and redirects to confirmation', function () {
+    $store = orderViewsStore();
+    $variant = orderViewsVariant($store);
+    $cart = app(CartService::class)->create($store);
+    session(['cart_id' => $cart->getKey()]);
+    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
+
+    $component = Livewire::test(CheckoutShow::class)
+        ->set('email', 'buyer@example.test')
+        ->set('shippingAddress.first_name', 'Test')
+        ->set('shippingAddress.last_name', 'Buyer')
+        ->set('shippingAddress.address1', 'Main Street 1')
+        ->set('shippingAddress.city', 'Berlin')
+        ->set('shippingAddress.country', 'DE')
+        ->set('shippingAddress.postal_code', '10115')
+        ->call('saveAddress')
+        ->set('selectedShippingRateId', orderViewsShippingRate($store)->getKey())
+        ->call('selectShippingMethod')
+        ->set('paymentMethod', 'credit_card')
+        ->call('selectPaymentMethod')
+        ->set('cardNumber', '4242 4242 4242 4242')
+        ->call('placeOrder');
+
+    $order = Order::withoutGlobalScopes()->firstOrFail();
+
+    $component->assertRedirect(route('checkout.confirmation', $order));
+
+    expect($order->email)->toBe('buyer@example.test')
+        ->and($order->lines)->toHaveCount(1)
+        ->and($cart->refresh()->status)->toBe(CartStatus::Converted)
+        ->and(session('last_order_id'))->toBe($order->getKey())
+        ->and(session('cart_id'))->toBeNull();
+});
+
+test('checkout page surfaces payment failures and releases reservations', function () {
+    $store = orderViewsStore();
+    $variant = orderViewsVariant($store);
+    $cart = app(CartService::class)->create($store);
+    session(['cart_id' => $cart->getKey()]);
+    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
+
+    Livewire::test(CheckoutShow::class)
+        ->set('email', 'buyer@example.test')
+        ->set('shippingAddress.first_name', 'Test')
+        ->set('shippingAddress.last_name', 'Buyer')
+        ->set('shippingAddress.address1', 'Main Street 1')
+        ->set('shippingAddress.city', 'Berlin')
+        ->set('shippingAddress.country', 'DE')
+        ->set('shippingAddress.postal_code', '10115')
+        ->call('saveAddress')
+        ->set('selectedShippingRateId', orderViewsShippingRate($store)->getKey())
+        ->call('selectShippingMethod')
+        ->set('paymentMethod', 'credit_card')
+        ->call('selectPaymentMethod')
+        ->set('cardNumber', '4000 0000 0000 0002')
+        ->call('placeOrder')
+        ->assertHasErrors('cardNumber')
+        ->assertSet('step', 'payment');
+
+    $checkout = Checkout::withoutGlobalScopes()->where('cart_id', $cart->getKey())->firstOrFail();
+    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
+
+    expect(Order::withoutGlobalScopes()->count())->toBe(0)
+        ->and($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
+        ->and($inventory->quantity_reserved)->toBe(0);
+});
+
+test('order confirmation is visible only for the session order or customer order', function () {
+    $store = orderViewsStore();
+    $customer = Customer::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
+    $order = Order::factory()->forCustomer($customer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $customer->getKey(),
+        'email' => $customer->email,
+    ]);
+
+    session(['last_order_id' => $order->getKey()]);
+
+    Livewire::test(Confirmation::class, ['order' => $order])
+        ->assertSee($order->order_number);
+
+    session()->forget('last_order_id');
+
+    Livewire::test(Confirmation::class, ['order' => $order])
+        ->assertStatus(404);
+});
+
+test('customer account lists and shows customer orders', function () {
+    $store = orderViewsStore();
+    $customer = Customer::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
+    $otherCustomer = Customer::factory()->create(['store_id' => $store->getKey()]);
+    $order = Order::factory()->forCustomer($customer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $customer->getKey(),
+        'order_number' => '#7777',
+    ]);
+    $otherOrder = Order::factory()->forCustomer($otherCustomer)->paid()->create([
+        'store_id' => $store->getKey(),
+        'customer_id' => $otherCustomer->getKey(),
+        'order_number' => '#8888',
+    ]);
+    OrderLine::factory()->create([
+        'order_id' => $order->getKey(),
+        'title_snapshot' => 'Classic Cotton T-Shirt',
+    ]);
+
+    $this->actingAs($customer, 'customer');
+
+    Livewire::test(AccountOrdersIndex::class)
+        ->assertSee('#7777')
+        ->assertDontSee('#8888');
+
+    Livewire::test(AccountOrderShow::class, ['order' => $order])
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('#7777');
+
+    Livewire::test(AccountOrderShow::class, ['order' => $otherOrder])
+        ->assertStatus(404);
 });


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Feature/Storefront/OrderViewsTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


message: Checkout completion and account order views are patched. I’m formatting and running the storefront order tests now; if they pass, I’ll build assets and browser-check the new flow.


text: Checkout completion and account order views are patched. I’m formatting and running the storefront order tests now; if they pass, I’ll build assets and browser-check the new flow.


cmd: vendor/bin/pint --dirty --format agent && php artisan test --compact tests/Feature/Storefront/OrderViewsTest.php


output:
Chunk ID: 4eeaf7
Wall time: 3.0789 seconds
Process exited with code 2
Original token count: 313
Output:
{"result":"fixed","files":[{"path":"tests\/Feature\/Storefront\/OrderViewsTest.php","fixers":["no_unused_imports"]},{"path":"routes\/web.php","fixers":["ordered_imports"]}]}
  ...⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Storefront\OrderViewsTest >…  ModelNotFoundException
  No query results for model [App\Models\Order].

  at vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:780
    776▕         if (! is_null($model = $this->first($columns))) {
    777▕             return $model;
    778▕         }
    779▕
  ➜ 780▕         throw (new ModelNotFoundException)->setModel(get_class($this->model));
    781▕     }
    782▕
    783▕     /**
    784▕      * Execute the query and get the first result or call a callback.

      +1 vendor frames
  2   app/Livewire/Storefront/Account/Orders/Show.php:26
      +13 vendor frames
  16  storage/framework/views/84d0b6c5f65dc224d28a59d8d831bb9c.php:12

  Tests:    1 failed, 3 passed (20 assertions)
  Duration: 2.26s


command: vendor/bin/pint --dirty --format agent && php artisan test --compact tests/Feature/Storefront/OrderViewsTest.php

aggregated_output:
{"result":"fixed","files":[{"path":"tests\/Feature\/Storefront\/OrderViewsTest.php","fixers":["no_unused_imports"]},{"path":"routes\/web.php","fixers":["ordered_imports"]}]}
  ...⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\Storefront\OrderViewsTest >…  ModelNotFoundException
  No query results for model [App\Models\Order].

  at vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:780
    776▕         if (! is_null($model = $this->first($columns))) {
    777▕             return $model;
    778▕         }
    779▕
  ➜ 780▕         throw (new ModelNotFoundException)->setModel(get_class($this->model));
    781▕     }
    782▕
    783▕     /**
    784▕      * Execute the query and get the first result or call a callback.

      +1 vendor frames
  2   app/Livewire/Storefront/Account/Orders/Show.php:26
      +13 vendor frames
  16  storage/framework/views/84d0b6c5f65dc224d28a59d8d831bb9c.php:12

  Tests:    1 failed, 3 passed (20 assertions)
  Duration: 2.26s

status: failed


cmd: nl -ba app/Livewire/Storefront/Account/Orders/Show.php | sed -n '1,80p' && sed -n '1,40p' tests/Feature/Storefront/OrderViewsTest.php


command: nl -ba app/Livewire/Storefront/Account/Orders/Show.php | sed -n '1,80p' && sed -n '1,40p' tests/Feature/Storefront/OrderViewsTest.php

OrderViewsTest.php

aggregated_output:
     1	<?php
     2
     3	namespace App\Livewire\Storefront\Account\Orders;
     4
     5	use App\Models\Customer;
     6	use App\Models\Order;
     7	use App\Models\Store;
     8	use Illuminate\Support\Facades\Auth;
     9	use Livewire\Component;
    10
    11	class Show extends Component
    12	{
    13	    public int $orderId;
    14
    15	    public function mount(Order $order): void
    16	    {
    17	        $store = app('current_store');
    18	        $customer = Auth::guard('customer')->user();
    19
    20	        abort_unless($store instanceof Store && $customer instanceof Customer, 404);
    21
    22	        $order = Order::withoutGlobalScopes()
    23	            ->where('store_id', $store->getKey())
    24	            ->where('customer_id', $customer->getKey())
    25	            ->whereKey($order->getKey())
    26	            ->firstOrFail();
    27
    28	        $this->orderId = $order->getKey();
    29	    }
    30
    31	    public function render(): mixed
    32	    {
    33	        return view('livewire.storefront.account.orders.show', [
    34	            'order' => $this->order(),
    35	        ])->layout('layouts.storefront', [
    36	            'title' => 'Order details',
    37	        ]);
    38	    }
    39
    40	    private function order(): Order
    41	    {
    42	        return Order::withoutGlobalScopes()
    43	            ->with(['lines', 'payments', 'refunds', 'fulfillments.lines'])
    44	            ->findOrFail($this->orderId);
    45	    }
    46	}
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Livewire\Storefront\Account\Orders\Index as AccountOrdersIndex;
use App\Livewire\Storefront\Account\Orders\Show as AccountOrderShow;
use App\Livewire\Storefront\Checkout\Confirmation;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function orderViewsStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

function orderViewsVariant(Store $store): ProductVariant
{
    $product = Product::withoutGlobalScopes()