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

aggregated_output:
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Show extends Component
{
    #[Locked]
    public int $storeId;

    public string $step = 'address';

    public string $email = '';

    /**
     * @var array<string, string>
     */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    /**
     * @var array<string, string>
     */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public bool $billingSame = true;

    public ?int $selectedShippingRateId = null;

    public string $discountCode = '';

    public string $paymentMethod = 'credit_card';

    public string $cardNumber = '';

    public string $cardName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public function mount(): void
    {
        $this->storeId = $this->store()->getKey();
        $this->email = $this->customer()?->email ?? '';

        $this->fillFromCheckout($this->checkout());
    }

    public function saveAddress(): void
    {
        $this->validate([
            'email' => ['required', 'email'],
            'shippingAddress.first_name' => ['required', 'string'],
            'shippingAddress.last_name' => ['required', 'string'],
            'shippingAddress.address1' => ['required', 'string'],
            'shippingAddress.city' => ['required', 'string'],
            'shippingAddress.country' => ['required', 'string', 'size:2'],
            'shippingAddress.postal_code' => [
                'required',
                'string',
                function (string $attribute, mixed $value, Closure $fail): void {
                    if (strtoupper($this->shippingAddress['country']) === 'DE' && preg_match('/^\d{5}$/', (string) $value) !== 1) {
                        $fail('The postal code format is invalid.');
                    }
                },
            ],
            'billingSame' => ['boolean'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            $checkout = app(CheckoutService::class)->setAddress($checkout, [
                'email' => $this->email,
                'shipping_address' => $this->shippingAddress,
                'billing_address' => $this->billingSame ? $this->shippingAddress : $this->billingAddress,
            ]);

            $this->selectedShippingRateId = null;

            if (! $this->requiresShipping()) {
                app(CheckoutService::class)->setShippingMethod($checkout, null);
                $this->step = 'payment';

                return;
            }

            $this->step = 'shipping';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'email' => $exception->getMessage(),
            ]);
        }
    }

    public function selectShippingMethod(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        if ($this->requiresShipping()) {
            $this->validate([
                'selectedShippingRateId' => ['required', 'integer'],
            ]);
        }

        try {
            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
            $this->step = 'payment';
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            throw ValidationException::withMessages([
                'selectedShippingRateId' => $exception->getMessage(),
            ]);
        }
    }

    public function applyDiscount(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        $checkout->forceFill([
            'discount_code' => trim($this->discountCode) !== '' ? trim($this->discountCode) : null,
        ])->save…3507 chars truncated…es.option',
        ]);
    }

    public function checkout(): ?Checkout
    {
        $cart = $this->cart();

        if (! $cart instanceof Cart || $cart->lines->isEmpty()) {
            return null;
        }

        $checkout = Checkout::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->whereNotIn('status', [CheckoutStatus::Completed->value, CheckoutStatus::Expired->value])
            ->latest('id')
            ->first();

        if (! $checkout instanceof Checkout) {
            $checkout = app(CheckoutService::class)->createFromCart($cart, $this->customer());
        }

        $checkout = $this->syncSessionDiscount($checkout);

        return $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.optionValues.option']);
    }

    /**
     * @return Collection<int, CartLine>
     */
    public function lines(): Collection
    {
        return $this->cart()?->lines->sortBy('id')->values() ?? collect();
    }

    /**
     * @return Collection<int, ShippingRate>
     */
    public function availableRates(): Collection
    {
        if ($this->shippingAddress['country'] === '') {
            return collect();
        }

        return app(ShippingCalculator::class)->getAvailableRates($this->store(), $this->shippingAddress);
    }

    public function requiresShipping(): bool
    {
        $cart = $this->cart();

        return $cart instanceof Cart && app(ShippingCalculator::class)->requiresShipping($cart);
    }

    /**
     * @return array<int, int>
     */
    public function shippingRateAmounts(): array
    {
        $cart = $this->cart();

        if (! $cart instanceof Cart) {
            return [];
        }

        return $this->availableRates()
            ->mapWithKeys(fn (ShippingRate $rate): array => [
                $rate->getKey() => app(ShippingCalculator::class)->calculate($rate, $cart) ?? 0,
            ])
            ->all();
    }

    public function lineCount(): int
    {
        return $this->lines()->sum('quantity');
    }

    public function subtotal(): int
    {
        return $this->lines()->sum('line_subtotal_amount');
    }

    /**
     * @return array<string, mixed>
     */
    public function totals(): array
    {
        $cart = $this->cart();

        return $this->checkout()?->totals_json ?? [
            'subtotal' => $this->subtotal(),
            'discount' => 0,
            'shipping' => 0,
            'tax' => 0,
            'total' => $this->subtotal(),
            'currency' => $cart?->currency ?? $this->store()->default_currency,
        ];
    }

    public function render(): mixed
    {
        return view('livewire.storefront.checkout.show', [
            'cart' => $this->cart(),
            'checkout' => $this->checkout(),
            'lines' => $this->lines(),
            'lineCount' => $this->lineCount(),
            'rates' => $this->availableRates(),
            'rateAmounts' => $this->shippingRateAmounts(),
            'requiresShipping' => $this->requiresShipping(),
            'totals' => $this->totals(),
        ])->layout('layouts.storefront', [
            'title' => 'Checkout',
        ]);
    }

    private function customer(): ?Customer
    {
        $customer = Auth::guard('customer')->user();

        return $customer instanceof Customer ? $customer : null;
    }

    private function fillFromCheckout(?Checkout $checkout): void
    {
        if (! $checkout instanceof Checkout) {
            return;
        }

        $this->email = $checkout->email ?: $this->email;
        $this->shippingAddress = array_replace($this->shippingAddress, $checkout->shipping_address_json ?? []);
        $this->billingAddress = array_replace($this->billingAddress, $checkout->billing_address_json ?? []);
        $this->selectedShippingRateId = $checkout->shipping_method_id;
        $this->discountCode = (string) ($checkout->discount_code ?? '');
        $this->paymentMethod = (string) ($checkout->payment_method ?? $this->paymentMethod);
        $this->step = match ($checkout->status) {
            CheckoutStatus::Started => 'address',
            CheckoutStatus::Addressed => 'shipping',
            CheckoutStatus::ShippingSelected => 'payment',
            CheckoutStatus::PaymentSelected => 'reserved',
            default => 'address',
        };
    }

    private function syncSessionDiscount(Checkout $checkout): Checkout
    {
        $code = trim((string) session('cart_discount_code'));

        if ($code === '' || $checkout->discount_code !== null) {
            return $checkout;
        }

        $checkout->forceFill(['discount_code' => $code])->save();

        try {
            app(PricingEngine::class)->calculate($checkout);
        } catch (InvalidDiscountException) {
            $checkout->forceFill(['discount_code' => null])->save();
            session()->forget('cart_discount_code');
            app(PricingEngine::class)->calculate($checkout);
        }

        return $checkout->refresh();
    }
}


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

Confirmation.php

type: read

aggregated_output:
<?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\Attributes\Locked;
use Livewire\Component;

class Confirmation extends Component
{
    #[Locked]
    public int $storeId;

    #[Locked]
    public int $orderId;

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

        abort_unless($store instanceof Store, 404);

        $order = Order::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($order->getKey())
            ->first();

        abort_unless($order instanceof Order, 404);

        $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->storeId = $store->getKey();
        $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
    {
        $customer = Auth::guard('customer')->user();
        $sessionOrderId = session('last_order_id');

        abort_if(! $customer instanceof Customer && $sessionOrderId === null, 404);

        return Order::withoutGlobalScopes()
            ->with(['lines', 'payments', 'fulfillments.lines'])
            ->where('store_id', $this->storeId)
            ->whereKey($this->orderId)
            ->where(function ($query) use ($customer, $sessionOrderId): void {
                if ($customer instanceof Customer) {
                    $query->orWhere('customer_id', $customer->getKey());
                }

                if ($sessionOrderId !== null) {
                    $query->orWhere('id', (int) $sessionOrderId);
                }
            })
            ->firstOrFail();
    }
}

status: completed


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

confirmation.blade.php

aggregated_output:
    @php
        $paymentMethodLabel = match ($order->payment_method) {
            \App\Enums\PaymentMethod::CreditCard => 'Credit Card',
            \App\Enums\PaymentMethod::Paypal => 'PayPal',
            \App\Enums\PaymentMethod::BankTransfer => 'Bank Transfer',
        };
    @endphp

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

                    Thank you for your order!

                    Order {{ $order->order_number }} has been placed. 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" />

                    Payment method
                    {{ $paymentMethodLabel }}

                    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>


command: sed -n '1,260p' tests/Feature/Checkout/CheckoutServiceTest.php

CheckoutServiceTest.php

aggregated_output:
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

function checkoutStore(): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    TaxSettings::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => ['default_rate_bps' => 0, 'shipping_taxable' => false],
    ]);

    return $store;
}

function checkoutVariant(Store $store, bool $requiresShipping = true): ProductVariant
{
    $product = Product::factory()
        ->withDefaultVariant(2500)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $variant->forceFill([
        'requires_shipping' => $requiresShipping,
        'weight_g' => $requiresShipping ? 500 : 0,
    ])->save();
    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function checkoutAddress(string $country = 'DE'): array
{
    return [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => $country,
            'postal_code' => '10115',
        ],
    ];
}

test('checkout service transitions through address shipping payment and expiry', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);

    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    expect($checkout->status)->toBe(CheckoutStatus::Addressed)
        ->and($checkout->email)->toBe('buyer@example.test');

    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBe($rate->getKey());

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->expires_at)->not->toBeNull()
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    $expired = app(CheckoutService::class)->expireCheckout($checkout);
    expect($expired->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('checkout service rejects unserviceable shipping addresses for physical carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress('FR'));

    expect(fn () => app(CheckoutService::class)->setShippingMethod($checkout, null))
        ->toThrow(UnserviceableShippingAddressException::class);
});

test('checkout service skips shipping for digital-only carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store, requiresShipping: false);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, null);

    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBeNull()
        ->and($checkout->totals_json['shipping'])->toBe(0);
});

test('expire abandoned checkouts job expires stale checkouts and releases reservations', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    $checkout->forceFill(['expires_at' => now()->subMinute()])->save();

    (new ExpireAbandonedCheckouts)->handle(app(CheckoutService::class));

    expect($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('cleanup abandoned carts job abandons old carts and expires related checkouts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'paypal');
    $cart->forceFill(['updated_at' => now()->subDays(15)])->save();

    (new CleanupAbandonedCarts)->handle(app(CheckoutService::class));

    expect($cart->refresh()->status)->toBe(CartStatus::Abandoned)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});


command: sed -n '1,280p' tests/Feature/Checkout/OrderServiceTest.php

OrderServiceTest.php

aggregated_output: sed: tests/Feature/Checkout/OrderServiceTest.php: No such file or directory

status: failed


command: rg -n "checkout.confirmation|checkout.show|/checkout/confirmation|/checkout/\{|checkout/\{checkout|confirmation" tests app resources routes specs

type: search

cmd: rg -n "checkout.confirmation|checkout.show|/checkout/confirmation|/checkout/\\{|checkout/\\{checkout|confirmation" tests app resources routes specs

query: checkout.confirmation|checkout.show|/checkout/confirmation|/checkout/\{|checkout/\{checkout|confirmation

path: tests

aggregated_output:
routes/web.php:61:    Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
routes/web.php:62:    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
specs/06-AUTH-AND-SECURITY.md:86:    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
specs/06-AUTH-AND-SECURITY.md:169:    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
specs/06-AUTH-AND-SECURITY.md:184:| password_confirmation | string | Y | must match password |
specs/06-AUTH-AND-SECURITY.md:881:- Password validation rules must enforce minimum 8 characters and use the `confirmed` rule (requiring a matching `password_confirmation` field).
specs/05-BUSINESS-LOGIC.md:987:            Backend-->>Storefront: 200 OK (order confirmation)
specs/05-BUSINESS-LOGIC.md:996:        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
specs/05-BUSINESS-LOGIC.md:1016:When `payment_method = bank_transfer`, the order confirmation page and email display mock bank transfer details:
specs/05-BUSINESS-LOGIC.md:1150:    5. On provider confirmation:
specs/05-BUSINESS-LOGIC.md:1177:| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
specs/05-BUSINESS-LOGIC.md:1266:When a payment is confirmed (either instant for credit_card/paypal, or via admin confirmation for bank_transfer):
specs/05-BUSINESS-LOGIC.md:1594:| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
specs/05-BUSINESS-LOGIC.md:1604:| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
specs/04-STOREFRONT-UI.md:53:| Checkout | Purchase flow | checkout index, order confirmation |
specs/04-STOREFRONT-UI.md:938:## 8. Checkout Page (GET /checkout/{checkoutId})
specs/04-STOREFRONT-UI.md:1113:| Success (card/paypal) | On successful instant capture, redirect to confirmation page. |
specs/04-STOREFRONT-UI.md:1114:| Success (bank_transfer) | On successful order placement, redirect to confirmation page (which shows bank transfer instructions). |
specs/04-STOREFRONT-UI.md:1160:## 9. Order Confirmation (GET /checkout/{checkoutId}/confirmation)
specs/04-STOREFRONT-UI.md:1171:|  "We've sent a confirmation to you@email.com"                 |
specs/09-IMPLEMENTATION-ROADMAP.md:402:| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:595:| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |
specs/09-IMPLEMENTATION-ROADMAP.md:1181:| registers a new customer | POST /account/register with name, email, password, password_confirmation | customer created in DB, auto-logged in, redirected to /account | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1432:| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1471:| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1788:| `Storefront/Checkout/Confirmation` | Order confirmation |
specs/09-IMPLEMENTATION-ROADMAP.md:1907:| `Payments/BankTransferConfirmationTest` | Bank transfer confirmation and auto-cancel tests |
specs/02-API-ROUTES.md:192:| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
specs/02-API-ROUTES.md:193:| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |
specs/02-API-ROUTES.md:446:        API-->>C: 200 (order confirmation data)
specs/02-API-ROUTES.md:454:        API-->>C: 200 (order confirmation + bank transfer instructions)
specs/02-API-ROUTES.md:960:Retrieve order status. Access is controlled via a signed token (included in confirmation emails and the confirmation page URL).
specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | complete | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admi…15982 chars truncated…/Feature/Auth/PasswordResetTest.php:79:            'password_confirmation' => 'password',
tests/Feature/Auth/CustomerPasswordResetTest.php:137:        'password_confirmation' => 'new-password',
tests/Feature/Auth/CustomerPasswordResetTest.php:197:        ->set('password_confirmation', 'new-password')
tests/Feature/Auth/CustomerPasswordResetTest.php:245:        ->set('password_confirmation', 'new-password')
tests/Feature/Auth/CustomerPasswordResetTest.php:261:        ->set('password_confirmation', 'new-password')
tests/Feature/Foundation/CustomerAuthTest.php:80:        ->set('password_confirmation', 'password')
tests/Feature/Foundation/CustomerAuthTest.php:95:        ->set('password_confirmation', 'password')
tests/Feature/Auth/RegistrationTest.php:16:        'password_confirmation' => 'password',
tests/Feature/Storefront/OrderViewsTest.php:59:test('checkout page places an order and redirects to confirmation', function () {
tests/Feature/Storefront/OrderViewsTest.php:88:    $component->assertRedirect(route('checkout.confirmation', $order));
tests/Feature/Storefront/OrderViewsTest.php:132:test('order confirmation is visible only for the session order or customer order', function () {
tests/Feature/Admin/SettingsManagementTest.php:195:        ->and(data_get($settings, 'notifications.order_confirmation_enabled'))->toBeFalse()
tests/Feature/Admin/SettingsManagementTest.php:196:        ->and(data_get($settings, 'notifications.shipping_confirmation_enabled'))->toBeTrue()
tests/Feature/Admin/SettingsManagementTest.php:197:        ->and(data_get($settings, 'notifications.refund_confirmation_enabled'))->toBeFalse()
tests/Feature/Settings/PasswordUpdateTest.php:20:        ->set('password_confirmation', 'new-password')
tests/Feature/Settings/PasswordUpdateTest.php:38:        ->set('password_confirmation', 'new-password')
tests/Feature/Settings/TwoFactorAuthenticationTest.php:31:test('two factor settings page requires password confirmation when enabled', function () {
tests/Feature/Settings/TwoFactorAuthenticationTest.php:52:test('two factor authentication disabled when confirmation abandoned between requests', function () {
tests/Browser/Storefront/CheckoutTest.php:123:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CheckoutTest.php:220:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CheckoutTest.php:233:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CustomerAccountTest.php:77:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:90:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:103:        ->fill('input[wire\\:model="password_confirmation"]', 'different456')
app/Livewire/Admin/Settings/Notifications.php:71:                    'order_confirmation_enabled' => $validated['orderConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:72:                    'shipping_confirmation_enabled' => $validated['shippingConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:73:                    'refund_confirmation_enabled' => $validated['refundConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:102:        $this->orderConfirmationEnabled = (bool) data_get($settings, 'notifications.order_confirmation_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:103:        $this->shippingConfirmationEnabled = (bool) data_get($settings, 'notifications.shipping_confirmation_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:104:        $this->refundConfirmationEnabled = (bool) data_get($settings, 'notifications.refund_confirmation_enabled', true);
app/Livewire/Storefront/Checkout/Confirmation.php:46:        return view('livewire.storefront.checkout.confirmation', [
app/Livewire/Storefront/Checkout/Confirmation.php:49:            'title' => 'Order confirmation',
app/Livewire/Storefront/Checkout/Show.php:252:            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
app/Livewire/Storefront/Checkout/Show.php:385:        return view('livewire.storefront.checkout.show', [
app/Livewire/Storefront/CartDrawer.php:69:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Storefront/Cart/Show.php:148:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Settings/Password.php:18:    public string $password_confirmation = '';
app/Livewire/Settings/Password.php:31:            $this->reset('current_password', 'password', 'password_confirmation');
app/Livewire/Settings/Password.php:40:        $this->reset('current_password', 'password', 'password_confirmation');
app/Livewire/Storefront/Account/Auth/Register.php:19:    public string $password_confirmation = '';
app/Livewire/Storefront/Account/Auth/ResetPassword.php:22:    public string $password_confirmation = '';


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

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Show extends Component
{
    #[Locked]
    public int $storeId;

    public string $step = 'address';

    public string $email = '';

    /**
     * @var array<string, string>
     */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    /**
     * @var array<string, string>
     */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public bool $billingSame = true;

    public ?int $selectedShippingRateId = null;

    public string $discountCode = '';

    public string $paymentMethod = 'credit_card';

    public string $cardNumber = '';

    public string $cardName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public function mount(): void
    {
        $this->storeId = $this->store()->getKey();
        $this->email = $this->customer()?->email ?? '';

        $this->fillFromCheckout($this->checkout());
    }

    public function saveAddress(): void
    {
        $this->validate([
            'email' => ['required', 'email'],
            'shippingAddress.first_name' => ['required', 'string'],
            'shippingAddress.last_name' => ['required', 'string'],
            'shippingAddress.address1' => ['required', 'string'],
            'shippingAddress.city' => ['required', 'string'],
            'shippingAddress.country' => ['required', 'string', 'size:2'],
            'shippingAddress.postal_code' => [
                'required',
                'string',
                function (string $attribute, mixed $value, Closure $fail): void {
                    if (strtoupper($this->shippingAddress['country']) === 'DE' && preg_match('/^\d{5}$/', (string) $value) !== 1) {
                        $fail('The postal code format is invalid.');
                    }
                },
            ],
            'billingSame' => ['boolean'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            $checkout = app(CheckoutService::class)->setAddress($checkout, [
                'email' => $this->email,
                'shipping_address' => $this->shippingAddress,
                'billing_address' => $this->billingSame ? $this->shippingAddress : $this->billingAddress,
            ]);

            $this->selectedShippingRateId = null;

            if (! $this->requiresShipping()) {
                app(CheckoutService::class)->setShippingMethod($checkout, null);
                $this->step = 'payment';

                return;
            }

            $this->step = 'shipping';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'email' => $exception->getMessage(),
            ]);
        }
    }

    public function selectShippingMethod(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        if ($this->requiresShipping()) {
            $this->validate([
                'selectedShippingRateId' => ['required', 'integer'],
            ]);
        }

        try {
            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
            $this->step = 'payment';
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            throw ValidationException::withMessages([
                'selectedShippingRateId' => $exception->getMessage(),
            ]);
        }
    }

    public function applyDiscount(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        $checkout->forceFill([
            'discount_code' => trim($this->discountCode) !== '' ? trim($this->discountCode) : null,
        ])->save();

        try {
            app(PricingEngine::class)->calculate($checkout);
            $this->resetErrorBag('discountCode');
        } catch (InvalidDiscountException $exception) {
            $checkout->forceFill(['discount_code' => null])->save();
            $this->discountCode = '';
            app(PricingEngine::class)->calculate($checkout);

            throw ValidationException::withMessages([
                'discountCode' => $exception->getMessage(),
            ]);
        }
    }

    public function selectPaymentMethod(): void
    {
        $this->validate([
            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
            $this->step = 'reserved';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'paymentMethod' => $exception->getMessage(),
            ]);
        }
    }

    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
    {
        if (isset($this->storeId)) {
            $store = Store::query()->findOrFail($this->storeId);
            app()->instance('current_store', $store);

            return $store;
        }

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    public function cart(): ?Cart
    {
        $cart = app(CartService::class)->currentForSession($this->store(), $this->customer());

        return $cart?->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }

    public function checkout(): ?Checkout
    {
        $cart = $this->cart();

        if (! $cart instanceof Cart || $cart->lines->isEmpty()) {
            return null;
        }

        $checkout = Checkout::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->whereNotIn('status', [CheckoutStatus::Completed->value, CheckoutStatus::Expired->value])
            ->latest('id')
            ->first();

        if (! $checkout instanceof Checkout) {
            $checkout = app(CheckoutService::class)->createFromCart($cart, $this->customer());
        }

        $checkout = $this->syncSessionDiscount($checkout);

        return $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.optionValues.option']);
    }

    /**
     * @return Collection<int, CartLine>
     */
    public function lines(): Collection
    {
        return $this->cart()?->lines->sortBy('id')->values() ?? collect();
    }

    /**
     * @return Collection<int, ShippingRate>
     */
    public function availableRates(): Collection
    {
        if ($this->shippingAddress['country'] === '') {
            return collect();
        }

        return app(ShippingCalculator::class)->getAvailableRates($this->store(), $this->shippingAddress);
    }

    public function requiresShipping(): bool
    {
        $cart = $this->cart();

        return $cart instanceof Cart && app(ShippingCalculator::class)->requiresShipping($cart);
    }

    /**
     * @return array<int, int>
     */
    public function shippingRateAmounts(): array
    {
        $cart = $this->cart();

        if (! $cart instanceof Cart) {
            return [];
        }

        return $this->availableRates()
            ->mapWithKeys(fn (ShippingRate $rate): array => [
                $rate->getKey() => app(ShippingCalculator::class)->calculate($rate, $cart) ?? 0,
            ])
            ->all();
    }

    public function lineCount(): int
    {
        return $this->lines()->sum('quantity');
    }

    public function subtotal(): int
    {
        return $this->lines()->sum('line_subtotal_amount');
    }

    /**
     * @return array<string, mixed>
     */
    public function totals(): array
    {
        $cart = $this->cart();

        return $this->checkout()?->totals_json ?? [
            'subtotal' => $this->subtotal(),
            'discount' => 0,
            'shipping' => 0,
            'tax' => 0,
            'total' => $this->subtotal(),
            'currency' => $cart?->currency ?? $this->store()->default_currency,
        ];
    }

    public function render(): mixed
    {
        return view('livewire.storefront.checkout.show', [
            'cart' => $this->cart(),
            'checkout' => $this->checkout(),
            'lines' => $this->lines(),
            'lineCount' => $this->lineCount(),
            'rates' => $this->availableRates(),
            'rateAmounts' => $this->shippingRateAmounts(),
            'requiresShipping' => $this->requiresShipping(),
            'totals' => $this->totals(),
        ])->layout('layouts.storefront', [
            'title' => 'Checkout',
        ]);
    }

    private function customer(): ?Customer
    {
        $customer = Auth::guard('customer')->user();

        return $customer instanceof Customer ? $customer : null;
    }

    private function fillFromCheckout(?Checkout $checkout): void
    {
        if (! $checkout instanceof Checkout) {
            return;
        }

        $this->email = $checkout->email ?: $this->email;
        $this->shippingAddress = array_replace($this->shippingAddress, $checkout->shipping_address_json ?? []);
        $this->billingAddress = array_replace($this->billingAddress, $checkout->billing_address_json ?? []);
        $this->selectedShippingRateId = $checkout->shipping_method_id;
        $this->discountCode = (string) ($checkout->discount_code ?? '');
        $this->paymentMethod = (string) ($checkout->payment_method ?? $this->paymentMethod);
        $this->step = match ($checkout->status) {
            CheckoutStatus::Started => 'address',
            CheckoutStatus::Addressed => 'shipping',
            CheckoutStatus::ShippingSelected => 'payment',
            CheckoutStatus::PaymentSelected => 'reserved',
            default => 'address',
        };
    }

    private function syncSessionDiscount(Checkout $checkout): Checkout
    {
        $code = trim((string) session('cart_discount_code'));

        if ($code === '' || $checkout->discount_code !== null) {
            return $checkout;
        }

        $checkout->forceFill(['discount_code' => $code])->save();

        try {
            app(PricingEngine::class)->calculate($checkout);
        } catch (InvalidDiscountException) {
            $checkout->forceFill(['discount_code' => null])->save();
            session()->forget('cart_discount_code');
            app(PricingEngine::class)->calculate($checkout);
        }

        return $checkout->refresh();
    }
}


output:
Chunk ID: 2476d3
Wall time: 0.0005 seconds
Process exited with code 0
Original token count: 565
Output:
<?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\Attributes\Locked;
use Livewire\Component;

class Confirmation extends Component
{
    #[Locked]
    public int $storeId;

    #[Locked]
    public int $orderId;

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

        abort_unless($store instanceof Store, 404);

        $order = Order::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($order->getKey())
            ->first();

        abort_unless($order instanceof Order, 404);

        $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->storeId = $store->getKey();
        $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
    {
        $customer = Auth::guard('customer')->user();
        $sessionOrderId = session('last_order_id');

        abort_if(! $customer instanceof Customer && $sessionOrderId === null, 404);

        return Order::withoutGlobalScopes()
            ->with(['lines', 'payments', 'fulfillments.lines'])
            ->where('store_id', $this->storeId)
            ->whereKey($this->orderId)
            ->where(function ($query) use ($customer, $sessionOrderId): void {
                if ($customer instanceof Customer) {
                    $query->orWhere('customer_id', $customer->getKey());
                }

                if ($sessionOrderId !== null) {
                    $query->orWhere('id', (int) $sessionOrderId);
                }
            })
            ->firstOrFail();
    }
}


output:
Chunk ID: 7f6e48
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1546
Output:

    @php
        $paymentMethodLabel = match ($order->payment_method) {
            \App\Enums\PaymentMethod::CreditCard => 'Credit Card',
            \App\Enums\PaymentMethod::Paypal => 'PayPal',
            \App\Enums\PaymentMethod::BankTransfer => 'Bank Transfer',
        };
    @endphp

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

                    Thank you for your order!

                    Order {{ $order->order_number }} has been placed. 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" />

                    Payment method
                    {{ $paymentMethodLabel }}

                    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>


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

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

function checkoutStore(): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    TaxSettings::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => ['default_rate_bps' => 0, 'shipping_taxable' => false],
    ]);

    return $store;
}

function checkoutVariant(Store $store, bool $requiresShipping = true): ProductVariant
{
    $product = Product::factory()
        ->withDefaultVariant(2500)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $variant->forceFill([
        'requires_shipping' => $requiresShipping,
        'weight_g' => $requiresShipping ? 500 : 0,
    ])->save();
    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function checkoutAddress(string $country = 'DE'): array
{
    return [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => $country,
            'postal_code' => '10115',
        ],
    ];
}

test('checkout service transitions through address shipping payment and expiry', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);

    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    expect($checkout->status)->toBe(CheckoutStatus::Addressed)
        ->and($checkout->email)->toBe('buyer@example.test');

    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBe($rate->getKey());

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->expires_at)->not->toBeNull()
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    $expired = app(CheckoutService::class)->expireCheckout($checkout);
    expect($expired->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('checkout service rejects unserviceable shipping addresses for physical carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress('FR'));

    expect(fn () => app(CheckoutService::class)->setShippingMethod($checkout, null))
        ->toThrow(UnserviceableShippingAddressException::class);
});

test('checkout service skips shipping for digital-only carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store, requiresShipping: false);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, null);

    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBeNull()
        ->and($checkout->totals_json['shipping'])->toBe(0);
});

test('expire abandoned checkouts job expires stale checkouts and releases reservations', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    $checkout->forceFill(['expires_at' => now()->subMinute()])->save();

    (new ExpireAbandonedCheckouts)->handle(app(CheckoutService::class));

    expect($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('cleanup abandoned carts job abandons old carts and expires related checkouts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'paypal');
    $cart->forceFill(['updated_at' => now()->subDays(15)])->save();

    (new CleanupAbandonedCarts)->handle(app(CheckoutService::class));

    expect($cart->refresh()->status)->toBe(CartStatus::Abandoned)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});


output:
Chunk ID: 8479cc
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 19
Output:
sed: tests/Feature/Checkout/OrderServiceTest.php: No such file or directory


output:
Chunk ID: 672887
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 6496
Output:
routes/web.php:61:    Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
routes/web.php:62:    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
specs/06-AUTH-AND-SECURITY.md:86:    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
specs/06-AUTH-AND-SECURITY.md:169:    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
specs/06-AUTH-AND-SECURITY.md:184:| password_confirmation | string | Y | must match password |
specs/06-AUTH-AND-SECURITY.md:881:- Password validation rules must enforce minimum 8 characters and use the `confirmed` rule (requiring a matching `password_confirmation` field).
specs/05-BUSINESS-LOGIC.md:987:            Backend-->>Storefront: 200 OK (order confirmation)
specs/05-BUSINESS-LOGIC.md:996:        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
specs/05-BUSINESS-LOGIC.md:1016:When `payment_method = bank_transfer`, the order confirmation page and email display mock bank transfer details:
specs/05-BUSINESS-LOGIC.md:1150:    5. On provider confirmation:
specs/05-BUSINESS-LOGIC.md:1177:| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
specs/05-BUSINESS-LOGIC.md:1266:When a payment is confirmed (either instant for credit_card/paypal, or via admin confirmation for bank_transfer):
specs/05-BUSINESS-LOGIC.md:1594:| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
specs/05-BUSINESS-LOGIC.md:1604:| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
specs/04-STOREFRONT-UI.md:53:| Checkout | Purchase flow | checkout index, order confirmation |
specs/04-STOREFRONT-UI.md:938:## 8. Checkout Page (GET /checkout/{checkoutId})
specs/04-STOREFRONT-UI.md:1113:| Success (card/paypal) | On successful instant capture, redirect to confirmation page. |
specs/04-STOREFRONT-UI.md:1114:| Success (bank_transfer) | On successful order placement, redirect to confirmation page (which shows bank transfer instructions). |
specs/04-STOREFRONT-UI.md:1160:## 9. Order Confirmation (GET /checkout/{checkoutId}/confirmation)
specs/04-STOREFRONT-UI.md:1171:|  "We've sent a confirmation to you@email.com"                 |
specs/09-IMPLEMENTATION-ROADMAP.md:402:| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:595:| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |
specs/09-IMPLEMENTATION-ROADMAP.md:1181:| registers a new customer | POST /account/register with name, email, password, password_confirmation | customer created in DB, auto-logged in, redirected to /account | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1432:| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1471:| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1788:| `Storefront/Checkout/Confirmation` | Order confirmation |
specs/09-IMPLEMENTATION-ROADMAP.md:1907:| `Payments/BankTransferConfirmationTest` | Bank transfer confirmation and auto-cancel tests |
specs/02-API-ROUTES.md:192:| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
specs/02-API-ROUTES.md:193:| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |
specs/02-API-ROUTES.md:446:        API-->>C: 200 (order confirmation data)
specs/02-API-ROUTES.md:454:        API-->>C: 200 (order confirmation + bank transfer instructions)
specs/02-API-ROUTES.md:960:Retrieve order status. Access is controlled via a signed token (included in confirmation emails and the confirmation page URL).
specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | complete | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes platform organization/store creation, store invite and membership endpoints, store-scoped product list/detail/create/update/archive/media presign, customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
specs/progress.md:32:| Storefront UI | `specs/04-STOREFRONT-UI.md` | complete | Storefront layout with desktop navigation and mobile hamburger flyout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus with one-level dropdown children, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates plus stock-limit feedback, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms at the spec root paths with account-path aliases, customer account overview/navigation/logout, customer order list/detail views, and customer address book list/create/update/default/delete controls render seeded and runtime data. |
specs/progress.md:33:| Business logic | `specs/05-BUSINESS-LOGIC.md` | complete | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, storefront cart stock-limit feedback for deny-policy inventory, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, customer address book management, customer logout session invalidation, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
specs/progress.md:36:| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | complete | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks all 18 Spec 08 browser suites with 143 tests: Suite 1 smoke pages/critical-page batch, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 3 admin product listing/create/edit/archive/search/status-filter/draft visibility interactions, Suite 4 admin order listing/filtering/detail/timeline/fulfillment/refund/bank-payment/guard/shipment interactions, Suite 5 admin discount seeded listing/percent/fixed/free-shipping creation/edit/status interactions, Suite 6 admin general/domain/shipping/tax settings interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, Suite 15 admin collection listing/create/edit interactions, Suite 16 admin customer listing/detail/order-history/address interactions, Suite 17 admin page listing/create/edit interactions, and Suite 18 admin analytics dashboard/KPI/funnel interactions. |
specs/progress.md:125:- 2026-05-04: `npm run build` passed after checkout confirmation and customer account Blade changes.
specs/progress.md:128:- 2026-05-04: Playwright MCP verified product add-to-cart through checkout order completion, `http://shop.test/checkout/confirmation/1`, `http://shop.test/account`, and `http://shop.test/account/orders/1`; order number, line item, totals, payment/fulfillment badges, and account order history rendered with no console warnings/errors.
specs/progress.md:451:- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after adding the checkout browser suite, domestic/international shipping fixtures, tax-inclusive storefront defaults, and checkout payment/confirmation UI updates.
specs/progress.md:615:- Order confirmation access is allowed for the authenticated owning customer or for the session that just placed the order via `last_order_id`.
specs/progress.md:624:- Bank transfer order completion creates a pending order and payment while keeping inventory reserved until the later admin payment-confirmation flow.
specs/progress.md:628:- Storefront order lookup uses a deterministic HMAC access token derived from store/order identity so confirmation links can be token-gated without adding a new runtime column.
specs/progress.md:665:- Automated browser coverage now combines smoke coverage with Suite 2 admin-auth interactions, Suite 3 admin product-management interactions, Suite 4 admin order-management interactions, Suite 5 admin discount-management interactions, Suite 6 admin settings interactions, Suite 7 storefront browsing interactions, Suite 8 cart-flow interactions, Suite 9 checkout-flow interactions, Suite 10 customer-account interactions, Suite 11 inventory-policy interactions, Suite 12 tenant-isolation interactions, Suite 13 responsive/mobile interactions, Suite 14 accessibility interactions, Suite 15 admin collection-management interactions, Suite 16 admin customer-management interactions, Suite 17 admin page-management interactions, and Suite 18 admin analytics interactions: storefront catalog/content/search/cart, storefront account auth/reset pages, the full authenticated admin navigation surface, admin login validation, protected-route redirects, logout, sidebar navigation, admin product listing/create/edit/archive/search/status-filter/draft visibility guards, admin order listing/filtering/detail/timeline/customer info, fulfillment creation, refund processing, bank-transfer payment confirmation, unpaid fulfillment guard, shipped/delivered fulfillment transitions, admin discount seeded-code listing, percent/fixed/free-shipping discount creation, discount value editing, status badges, admin store settings persistence, domain listing, seeded shipping zone/rate display, shipping rate creation, tax settings display and prices-include-tax persistence, admin collection seeded-listing, collection creation, collection description editing, admin customer listing, customer detail, customer order history, customer address section rendering, admin page seeded-listing, page creation, page body editing, admin analytics sidebar navigation, revenue/orders KPI labels, and conversion-funnel labels, mobile storefront rendering, seeded collection/product/search/static page browsing, sale/compare-at pricing, draft-product storefront visibility guards, stock/backorder messaging, main-navigation clicks, add-to-cart, cart quantity updates/removal, multi-product carts, subtotal/total display, valid/invalid/expired/maxed/free-shipping/fixed discount code handling, domestic/international checkout shipping, checkout discount carryover, address validation, empty-cart checkout protection, credit-card/PayPal/bank-transfer order completion, payment decline handling, payment-method UI switching, customer registration validation, customer login validation, account auth redirects, customer order history/detail, address book create/update, customer logout, deny-policy sold-out blocking, continue-policy backorder add-to-cart, in-stock status display, deny-policy cart stock-limit feedback, storefront product/collection/search store isolation, admin product/order store isolation, customer account store isolation, mobile hamburger navigation, mobile product/cart/checkout flows, mobile collection filtering, tablet admin login/sidebar navigation, console-clean home/cart pages, heading hierarchy, form labels, product image placeholder accessible text, checkout error linkage, keyboard navigation, and search input labeling.
specs/progress.md:667:- The admin order-management browser suite uses the deterministic seeded `#1001`, `#1002`, and `#1005` order fixtures; only inventory reservations needed by bank-transfer confirmation are normalized in the test setup.
specs/03-ADMIN-UI.md:363:| `bulkDelete()` | Soft-deletes selected products (with confirmation) |
specs/03-ADMIN-UI.md:367:| `confirmBulkDelete()` | Opens delete confirmation modal |
specs/03-ADMIN-UI.md:415:- "Delete" button (danger variant) - opens confirmation modal
specs/03-ADMIN-UI.md:450:| Delete confirmation | `modal` |
specs/03-ADMIN-UI.md:504:| `deleteProduct()` | Archives the product (with confirmation modal) |
specs/03-ADMIN-UI.md:683:| Delete confirmation | `modal` |
specs/03-ADMIN-UI.md:720:| `deleteCollection(int $id)` | Deletes a collection after confirmation |
specs/03-ADMIN-UI.md:1973:| `uninstallApp(int $appId)` | Uninstalls an app (with confirmation) |
specs/03-ADMIN-UI.md:2316:9. Delete action behind a confirmation Flux modal
specs/03-ADMIN-UI.md:2320:Every destructive action uses a Flux modal for confirmation:
specs/08-PLAYWRIGHT-E2E-PLAN.md:19:| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
specs/08-PLAYWRIGHT-E2E-PLAN.md:853:**Preconditions:** Logged in as admin, Order #1005 exists with financial_status=pending (before payment confirmation)
specs/08-PLAYWRIGHT-E2E-PLAN.md:1695:**Purpose:** Verify the complete multi-step checkout process: cart to contact info, shipping address, shipping method selection, payment, and order confirmation.
specs/08-PLAYWRIGHT-E2E-PLAN.md:1727:| 4. Payment | Payment section | Select payment method (Credit Card / PayPal / Bank Transfer), fill card details if credit card | Redirect to confirmation |
specs/08-PLAYWRIGHT-E2E-PLAN.md:2026:- Customer remains on the checkout page (NOT redirected to confirmation)
specs/08-PLAYWRIGHT-E2E-PLAN.md:2112:5. Enter password confirmation: `password123`
specs/08-PLAYWRIGHT-E2E-PLAN.md:2130:5. Enter password confirmation: `password123`
specs/08-PLAYWRIGHT-E2E-PLAN.md:2148:5. Enter password confirmation: `different456`
resources/views/livewire/settings/password.blade.php:23:                wire:model="password_confirmation"
resources/views/livewire/storefront/checkout/show.blade.php:200:
After placing your order, bank transfer instructions will be shown on the confirmation page.

resources/views/livewire/storefront/checkout/confirmation.blade.php:12:        ['label' => 'Checkout', 'url' => route('checkout.show')],
resources/views/livewire/storefront/checkout/confirmation.blade.php:60:
getKey() }}">
resources/views/livewire/storefront/account/auth/reset-password.blade.php:25:            wire:model="password_confirmation"
resources/views/livewire/admin/settings/notifications.blade.php:45:                    <flux:switch wire:model="orderConfirmationEnabled" label="Order confirmation" align="left" />
resources/views/livewire/admin/settings/notifications.blade.php:46:                    <flux:switch wire:model="shippingConfirmationEnabled" label="Shipping confirmation" align="left" />
resources/views/livewire/admin/settings/notifications.blade.php:47:                    <flux:switch wire:model="refundConfirmationEnabled" label="Refund confirmation" align="left" />
resources/views/livewire/storefront/account/auth/register.blade.php:33:            wire:model="password_confirmation"
resources/views/livewire/auth/register.blade.php:46:                name="password_confirmation"
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:52:            'settings_json.notifications.order_confirmation_enabled' => ['sometimes', 'boolean'],
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:53:            'settings_json.notifications.shipping_confirmation_enabled' => ['sometimes', 'boolean'],
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:54:            'settings_json.notifications.refund_confirmation_enabled' => ['sometimes', 'boolean'],
resources/views/livewire/auth/reset-password.blade.php:36:                name="password_confirmation"
tests/Feature/Auth/PasswordResetTest.php:60:            'password_confirmation' => 'password',
tests/Feature/Auth/PasswordResetTest.php:79:            'password_confirmation' => 'password',
tests/Feature/Auth/CustomerPasswordResetTest.php:137:        'password_confirmation' => 'new-password',
tests/Feature/Auth/CustomerPasswordResetTest.php:197:        ->set('password_confirmation', 'new-password')
tests/Feature/Auth/CustomerPasswordResetTest.php:245:        ->set('password_confirmation', 'new-password')
tests/Feature/Auth/CustomerPasswordResetTest.php:261:        ->set('password_confirmation', 'new-password')
tests/Feature/Foundation/CustomerAuthTest.php:80:        ->set('password_confirmation', 'password')
tests/Feature/Foundation/CustomerAuthTest.php:95:        ->set('password_confirmation', 'password')
tests/Feature/Auth/RegistrationTest.php:16:        'password_confirmation' => 'password',
tests/Feature/Storefront/OrderViewsTest.php:59:test('checkout page places an order and redirects to confirmation', function () {
tests/Feature/Storefront/OrderViewsTest.php:88:    $component->assertRedirect(route('checkout.confirmation', $order));
tests/Feature/Storefront/OrderViewsTest.php:132:test('order confirmation is visible only for the session order or customer order', function () {
tests/Feature/Admin/SettingsManagementTest.php:195:        ->and(data_get($settings, 'notifications.order_confirmation_enabled'))->toBeFalse()
tests/Feature/Admin/SettingsManagementTest.php:196:        ->and(data_get($settings, 'notifications.shipping_confirmation_enabled'))->toBeTrue()
tests/Feature/Admin/SettingsManagementTest.php:197:        ->and(data_get($settings, 'notifications.refund_confirmation_enabled'))->toBeFalse()
tests/Feature/Settings/PasswordUpdateTest.php:20:        ->set('password_confirmation', 'new-password')
tests/Feature/Settings/PasswordUpdateTest.php:38:        ->set('password_confirmation', 'new-password')
tests/Feature/Settings/TwoFactorAuthenticationTest.php:31:test('two factor settings page requires password confirmation when enabled', function () {
tests/Feature/Settings/TwoFactorAuthenticationTest.php:52:test('two factor authentication disabled when confirmation abandoned between requests', function () {
tests/Browser/Storefront/CheckoutTest.php:123:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CheckoutTest.php:220:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CheckoutTest.php:233:        ->assertPathBeginsWith('/checkout/confirmation')
tests/Browser/Storefront/CustomerAccountTest.php:77:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:90:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:103:        ->fill('input[wire\\:model="password_confirmation"]', 'different456')
app/Livewire/Admin/Settings/Notifications.php:71:                    'order_confirmation_enabled' => $validated['orderConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:72:                    'shipping_confirmation_enabled' => $validated['shippingConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:73:                    'refund_confirmation_enabled' => $validated['refundConfirmationEnabled'],
app/Livewire/Admin/Settings/Notifications.php:102:        $this->orderConfirmationEnabled = (bool) data_get($settings, 'notifications.order_confirmation_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:103:        $this->shippingConfirmationEnabled = (bool) data_get($settings, 'notifications.shipping_confirmation_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:104:        $this->refundConfirmationEnabled = (bool) data_get($settings, 'notifications.refund_confirmation_enabled', true);
app/Livewire/Storefront/Checkout/Confirmation.php:46:        return view('livewire.storefront.checkout.confirmation', [
app/Livewire/Storefront/Checkout/Confirmation.php:49:            'title' => 'Order confirmation',
app/Livewire/Storefront/Checkout/Show.php:252:            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
app/Livewire/Storefront/Checkout/Show.php:385:        return view('livewire.storefront.checkout.show', [
app/Livewire/Storefront/CartDrawer.php:69:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Storefront/Cart/Show.php:148:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Settings/Password.php:18:    public string $password_confirmation = '';
app/Livewire/Settings/Password.php:31:            $this->reset('current_password', 'password', 'password_confirmation');
app/Livewire/Settings/Password.php:40:        $this->reset('current_password', 'password', 'password_confirmation');
app/Livewire/Storefront/Account/Auth/Register.php:19:    public string $password_confirmation = '';
app/Livewire/Storefront/Account/Auth/ResetPassword.php:22:    public string $password_confirmation = '';


cmd: sed -n '1,160p' app/Services/WebhookService.php


command: sed -n '1,160p' app/Services/WebhookService.php

aggregated_output:
<?php

namespace App\Services;

use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\PersonalAccessToken;
use App\Models\Store;
use App\Models\User;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: PersonalAccessToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities, ?User $user = null): array
    {
        $user ??= auth()->user() instanceof User
            ? auth()->user()
            : $store->users()
                ->wherePivot('role', 'owner')
                ->firstOrFail();
        $plainText = 'shop_'.Str::random(48);
        $token = PersonalAccessToken::query()->create([
            'tokenable_type' => $user->getMorphClass(),
            'tokenable_id' => $user->getKey(),
            'name' => $name,
            'token' => hash('sha256', $plainText),
            'abilities' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


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

namespace App\Services;

use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\PersonalAccessToken;
use App\Models\Store;
use App\Models\User;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: PersonalAccessToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities, ?User $user = null): array
    {
        $user ??= auth()->user() instanceof User
            ? auth()->user()
            : $store->users()
                ->wherePivot('role', 'owner')
                ->firstOrFail();
        $plainText = 'shop_'.Str::random(48);
        $token = PersonalAccessToken::query()->create([
            'tokenable_type' => $user->getMorphClass(),
            'tokenable_id' => $user->getKey(),
            'name' => $name,
            'token' => hash('sha256', $plainText),
            'abilities' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


step: Finish critical auth/API guard alignment and tests

step: Patch Livewire/admin policy checks and tenant-safe pivots

status: pending

step: Patch checkout routes/idempotency/order race handling

step: Complete sanitizer tests and webhook integration verification

step: Run focused verification, update progress, then full audit gates

explanation: I’m turning the independent QA findings into a concrete fix set. The first priority is getting the bearer-token admin API contract consistent, then closing authorization, checkout idempotency, sanitizer, and verification gaps.


output: Plan updated


cmd: sed -n '1,180p' app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php


cmd: sed -n '1,220p' tests/Feature/Admin/AppsDevelopersTest.php


cmd: sed -n '1,280p' tests/Feature/Admin/OrderManagementTest.php


cmd: sed -n '1,240p' tests/Feature/Admin/ProductMediaManagementTest.php


cmd: sed -n '1,240p' tests/Feature/Admin/CollectionManagementTest.php