Output:
Wall time: 3.3193 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Forms Extracting input fields to Blade components\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## Extracting input fields to Blade components\n\nEven in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.\n\nIt can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.\n\nFor example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"text\" wire:model=\"title\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('title') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <input type=\"text\" wire:model=\"content\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('content') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nHere's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:\n\n```blade\n<form wire:submit=\"save\">\n    <x-input-text name=\"title\" wire:model=\"title\" /> <!-- [tl! highlight] -->\n\n    <x-input-text name=\"content\" wire:model=\"content\" /> <!-- [tl! highlight] -->\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nNext, here's the source for the `x-input-text` component:\n\n```blade\n<!-- resources/views/components/input-text.blade.php -->\n\n@props(['name'])\n\n<input type=\"text\" name=\"{{ $name }}\" {{ $attributes }}>\n\n<div>\n    @error($name) <span class=\"error\">{{ $message }}</span> @enderror\n</div>\n```\n\nAs you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.\n\nFor the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:\n\n* The `@props` directive\n* The `{{ $attributes }}` statement on the input\n\nLet's discuss each of these additions:\n\nBy specifying `name` as a \"prop\" using `@props(['name'])` we are telling Blade: if an attribute called \"name\" is set on this component, take its value and make it available inside this component as `$name`.\n\nFor other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for \"attribute forwarding\", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.\n\nThis ensures `wire:model=\"title\"` and any other extra attributes such as `disabled`, `class=\"...\"`, or `required` still get forwarded to the actual `<input>` element.\n\n### Custom form controls\n\nIn the previous example, we \"wrapped\" an input element into a re-usable Blade component we can use as if it was a native HTML input element.\n\nThis pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.\n\nFor example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple \"counter\" input written in Alpine.\n\nBefore we create a Blade component, let's first look at a simple, pure-Alpine, \"counter\" component for reference:\n\n```blade\n<div x-data=\"{ count: 0 }\">\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the component above shows a number alongside two buttons to increment and decrement that number.\n\nNow, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:\n\n```blade\n<x-input-counter wire:model=\"quantity\" />\n```\n\nCreating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.\n\nHowever, making it work with `wire:model=\"quantity\"` so that you can easily bind data from your Livewire component to the \"count\" inside this Alpine component needs one extra step.\n\nHere's the source for the component:\n\n```blade\n<!-- resources/view/components/input-counter.blade.php -->\n\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" {{ $attributes}}>\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the only different bit about this HTML is the `x-modelable=\"count\"` and `{{ $attributes }}`.\n\n`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)\n\n`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.\n\nBecause of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model=\"quantity\"` will be rendered alongside `x-modelable=\"count\"` on the root `<div>` of the Alpine component like so:\n\n```blade\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" wire:model=\"quantity\">\n```\n\n`x-modelable=\"count\"` tells Alpine to look for any `x-model` or `wire:model` statements and use \"count\" as the data to bind them to.\n\nBecause `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:\n\n```blade\n<x-input-counter x-model=\"quantity\" />\n```\n\nCreating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.\n\n---\n\n## Wire Transition Browser support\nWire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition\n\n## Browser support\n\nView Transitions are supported in Chrome 111+, Edge 111+, and Safari 18+. In browsers that don't support View Transitions, elements will appear and disappear without animation—the functionality still works, just without the visual transition.\n\n> [!warning] Firefox has limited support\n> Firefox 144+ supports basic view transitions, but does not support transition types.\n\n[View browser support on caniuse.com →](https://caniuse.com/view-transitions)\n\n---\n\n## Urls URLs for Named Routes Eloquent Models\nUrls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md\n\n\n#### Eloquent Models\n\nYou will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:\n\n```php\necho route('post.show', ['post' => $post]);\n```\n\n---\n\n## Wire Bind Common use cases\nWire Bind | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-bind\n\n## Common use cases\n\n### Binding styles\n\n```blade\n<div wire:bind:style=\"{ 'color': textColor, 'font-size': fontSize + 'px' }\">\n    Styled text\n</div>\n```\n\n### Binding href\n\n```blade\n<a wire:bind:href=\"url\">Dynamic link</a>\n```\n\n### Binding disabled state\n\n```blade\n<button wire:bind:disabled=\"isArchived\">Delete</button>\n```\n\n### Binding data attributes\n\n```blade\n<div wire:bind:data-count=\"count\">...</div>\n```\n\n---\n\n## Testing Browser testing\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## Browser testing\n\nPest v4 includes first-party browser testing support powered by Playwright. This allows you to test your Livewire components in a real browser, interacting with them just like a user would.\n\n### Installing browser testing\n\nFirst, install the Pest browser plugin:\n\n```shell\ncomposer require pestphp/pest-plugin-browser --dev\n```\n\nNext, install Playwright via npm:\n\n```shell\nnpm install playwright@latest\nnpx playwright install\n```\n\nFor complete browser testing documentation, see the [Pest browser testing guide](https://pestphp.com/docs/browser-testing).\n\n### Writing browser tests\n\nInstead of using `Livewire::test()`, you can use `Livewire::visit()` to test your component in a real browser:\n\n```php\nit('can create a new post', function () {\n    Livewire::visit('post.create')\n        ->type('[wire\\:model=\"title\"]', 'My first post')\n        ->type('[wire\\:model=\"content\"]', 'This is the content')\n        ->press('Save')\n        ->assertSee('Post created successfully');\n});\n```\n\nBrowser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.\n\nFor a complete list of available browser testing assertions, see the [Pest browser testing assertions](https://pestphp.com/docs/browser-testing#content-available-assertions).\n\n> [!info] When to use browser tests\n> Use browser tests for critical user flows and complex interactions. For most component testing, the standard `Livewire::test()` approach is faster and sufficient.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Models and UUIDv7\n\n**Likelihood Of Impact: Medium**\n\nThe `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids; // [tl! remove]\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasVersion4Uuids as HasUuids; // [tl! add]\n```\n\nThe `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.\n\n---\n\n## Blade Components Component Attributes\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n### Component Attributes\n\nWe've already examined how to pass data attributes to a component; however, sometimes you may need to specify additional HTML attributes, such as `class`, that are not part of the data required for a component to function. Typically, you want to pass these additional attributes down to the root element of the component template. For example, imagine we want to render an `alert` component like so:\n\n```blade\n<x-alert type=\"error\" :message=\"$message\" class=\"mt-4\"/>\n```\n\nAll of the attributes that are not part of the component's constructor will automatically be added to the component's \"attribute bag\". This attribute bag is automatically made available to the component via the `$attributes` variable. All of the attributes may be rendered within the component by echoing this variable:\n\n```blade\n<div {{ $attributes }}>\n    <!-- Component content -->\n</div>\n```\n\n> [!WARNING]\n> Using directives such as `@env` within component tags is not supported at this time. For example, `<x-alert :live=\"@env('production')\"/>` will not be compiled.\n\n---\n\n## Browser Testing assertScript\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertScript\n\nThe `assertScript` method asserts that the given JavaScript expression evaluates to the given value:\n\n```php\n$page->assertScript('document.title', 'Home Page');\n$page->assertScript('document.querySelector(\".btn\").disabled', true);\n```\n\n---\n\n## Eloquent Relationships Eager Loading Lazy Eager Loading\nEloquent Relationships | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-relationships.md\n\n\n### Lazy Eager Loading\n\nSometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models:\n\n```php\nuse App\\Models\\Book;\n\n$books = Book::all();\n\nif ($condition) {\n    $books->load('author', 'publisher');\n}\n```\n\nIf you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be closure instances which receive the query instance:\n\n```php\n$author->load(['books' => function ($query) {\n    $query->orderBy('published_date', 'asc');\n}]);\n```\n\nTo load a relationship only when it has not already been loaded, use the `loadMissing` method:\n\n```php\n$book->loadMissing('author');\n```\n\n---\n\n## Loading States Styling with Tailwind\nLoading States | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/loading-states\n\n## Styling with Tailwind\n\nTailwind v4 and above provides powerful selectors for working with the `data-loading` attribute.\n\n### Basic styling\n\nUse Tailwind's `data-loading:` variant to apply styles when an element is loading:\n\n```blade\n<button wire:click=\"save\" class=\"data-loading:opacity-50\">\n    Save\n</button>\n```\n\n### Showing elements during loading\n\nTo show an element only while loading is active, use the `not-data-loading:hidden` variant:\n\n```blade\n<button wire:click=\"save\">\n    Save\n</button>\n\n<span class=\"not-data-loading:hidden\">\n    Saving...\n</span>\n```\n\nThis approach is preferred over `hidden data-loading:block` because it works regardless of the element's display type (flex, inline, grid, etc.).\n\n### Styling children\n\nYou can style child elements when a parent has the `data-loading` attribute using the `in-data-loading:` variant:\n\n```blade\n<button wire:click=\"save\">\n    <span class=\"in-data-loading:hidden\">Save</span>\n    <span class=\"not-in-data-loading:hidden\">Saving...</span>\n</button>\n```\n\n> [!warning] The in-data-loading variant applies to all ancestors\n> The `in-data-loading:` variant will trigger if **any** ancestor element (no matter how far up the tree) has the `data-loading` attribute. This can lead to unexpected behavior if you have nested loading states.\n\n### Styling parents\n\nStyle parent elements when they contain a child with `data-loading` using the `has-data-loading:` variant:\n\n```blade\n<div class=\"has-data-loading:opacity-50\">\n    <button wire:click=\"save\">Save</button>\n</div>\n```\n\nWhen the button is clicked, the entire parent div will become semi-transparent.\n\n### Styling siblings\n\nYou can style sibling elements using Tailwind's `peer` utility with the `peer-data-loading:` variant:\n\n```blade\n<div>\n    <button wire:click=\"save\" class=\"peer\">\n        Save\n    </button>\n\n    <span class=\"peer-data-loading:opacity-50\">\n        Saving...\n    </span>\n</div>\n```\n\n### Complex selectors\n\nFor more advanced styling needs, you can use arbitrary variants to target specific elements:\n\n```blade\n<!-- Style all direct children when loading -->\n<div class=\"[&[data-loading]>*]:opacity-50\" wire:click=\"save\">\n    <span>Child 1</span>\n    <span>Child 2</span>\n</div>\n\n<!-- Style specific descendant elements -->\n<button class=\"[&[data-loading]_.icon]:animate-spin\" wire:click=\"save\">\n    \n    Save\n</button>\n```\n\nLearn more about Tailwind's state variants and arbitrary selectors in the [Tailwind CSS documentation](https://tailwindcss.com/docs/hover-focus-and-other-states).\n\n---\n\n## Styles Browser support\nStyles | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/styles\n\n## Browser support\n\nScoped styles use [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting), which is supported in all modern browsers (Chrome 120+, Firefox 117+, Safari 17.2+). For older browser support, consider using a CSS preprocessor or the `@assets` directive with pre-compiled stylesheets.\n\n---\n\n## Scout Searching Where Clauses Customizing the Eloquent Results Query\nScout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md\n\n\n#### Customizing the Eloquent Results Query\n\nAfter Scout retrieves a list of matching Eloquent models from your application's search engine, Eloquent is used to retrieve all of the matching models by their primary keys. You may customize this query by invoking the `query` method. The `query` method accepts a closure that will receive the Eloquent query builder instance as an argument:\n\n```php\nuse App\\Models\\Order;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n$orders = Order::search('Star Trek')\n    ->query(fn (Builder $query) => $query->with('invoices'))\n    ->get();\n```\n\nWhen using a third-party engine, this callback is invoked after the relevant models have already been retrieved from the search engine, so it should not be used for \"filtering\" results — use [Scout where clauses](#where-clauses) instead. However, when using the database engine, the `query` method's constraints are applied directly to the database query, so you may use it for filtering as well.\n\n---\n\n## Pagination Using custom pagination views\nPagination | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/pagination\n\n## Using custom pagination views\n\nIf you wish to bypass Livewire's pagination views entirely, you can render your own in one of two ways:\n\n1. The `->links()` method in your Blade view\n2. The `paginationView()` or `paginationSimpleView()` method in your component\n\n### Via `->links()`\n\nThe first approach is to simply pass your custom pagination Blade view name to the `->links()` method directly:\n\n```blade\n{{ $posts->links('custom-pagination-links') }}\n```\n\nWhen rendering the pagination links, Livewire will now look for a view at `resources/views/custom-pagination-links.blade.php`.\n\n### Via `paginationView()` or `paginationSimpleView()`\n\nThe second approach is to declare a `paginationView` or `paginationSimpleView` method inside your component which returns the name of the view you would like to use:\n\n```php\npublic function paginationView()\n{\n    return 'custom-pagination-links-view';\n}\n\npublic function paginationSimpleView()\n{\n    return 'custom-simple-pagination-links-view';\n}\n```\n\n### Sample pagination view\n\nBelow is an unstyled sample of a simple Livewire pagination view for your reference.\n\nAs you can see, you can use Livewire's page navigation helpers like `$this->nextPage()` directly inside your template by adding `wire:click=\"nextPage\"` to buttons:\n\n```blade\n<div>\n    @if ($paginator->hasPages())\n        <nav role=\"navigation\" aria-label=\"Pagination Navigation\">\n            <span>\n                @if ($paginator->onFirstPage())\n                    <span>Previous</span>\n                @else\n                    <button wire:click=\"previousPage\" wire:loading.attr=\"disabled\" rel=\"prev\">Previous</button>\n                @endif\n            </span>\n\n            <span>\n                @if ($paginator->onLastPage())\n                    <span>Next</span>\n                @else\n                    <button wire:click=\"nextPage\" wire:loading.attr=\"disabled\" rel=\"next\">Next</button>\n                @endif\n            </span>\n        </nav>\n    @endif\n</div>\n```\n\nFor visual-only loading states (like opacity changes), you can use Livewire's automatic `data-loading` attribute with Tailwind classes instead:\n\n```blade\n<button wire:click=\"nextPage\" class=\"data-loading:opacity-50\" rel=\"next\">\n    Next\n</button>\n```\n\n[Learn more about loading states →](/docs/4.x/loading-states)\n\n---\n\n## Installation Components not updating or errors in browser console\nInstallation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/installation\n\n## Components not updating or errors in browser console\n\n**Check the following:**\n- Ensure `@livewireStyles` is in the `<head>` of your layout\n- Ensure `@livewireScripts` is before `</body>` in your layout\n- Check your browser's developer console for JavaScript errors\n- Verify you're running a supported PHP version (8.1+) and Laravel version (10+)\n- Clear your application cache: `php artisan cache:clear`\n\nIf issues persist, check the [troubleshooting documentation](/docs/4.x/troubleshooting) for more detailed debugging steps.\n\n---\n\n## Responses Redirects Redirecting to Named Routes Populating Parameters via Eloquent Models\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Populating Parameters via Eloquent Models\n\nIf you are redirecting to a route with an \"ID\" parameter that is being populated from an Eloquent model, you may pass the model itself. The ID will be extracted automatically:\n\n```php\n// For a route with the following URI: /profile/{id}\n\nreturn redirect()->route('profile', [$user]);\n```\n\nIf you would like to customize the value that is placed in the route parameter, you can specify the column in the route parameter definition (`/profile/{id:slug}`) or you can override the `getRouteKey` method on your Eloquent model:\n\n```php\n/**\n * Get the value of the model's route key.\n */\npublic function getRouteKey(): mixed\n{\n    return $this->slug;\n}\n```\n\n---\n\n## Browser Testing assertNoJavaScriptErrors\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertNoJavaScriptErrors\n\nThe `assertNoJavaScriptErrors` method asserts there are no JavaScript errors on the page:\n\n```php\n$page->assertNoJavaScriptErrors();\n```\n\n---\n\n## Blade Components Component Attributes Non-Class Attribute Merging\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n#### Non-Class Attribute Merging\n\nWhen merging attributes that are not `class` attributes, the values provided to the `merge` method will be considered the \"default\" values of the attribute. However, unlike the `class` attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a `button` component's implementation may look like the following:\n\n```blade\n<button {{ $attributes->merge(['type' => 'button']) }}>\n    {{ $slot }}\n</button>\n```\n\nTo render the button component with a custom `type`, it may be specified when consuming the component. If no type is specified, the `button` type will be used:\n\n```blade\n<x-button type=\"submit\">\n    Submit\n</x-button>\n```\n\nThe rendered HTML of the `button` component in this example would be:\n\n```blade\n<button type=\"submit\">\n    Submit\n</button>\n```\n\nIf you would like an attribute other than `class` to have its default value and injected values joined together, you may use the `prepends` method. In this example, the `data-controller` attribute will always begin with `profile-controller` and any additional injected `data-controller` values will be placed after this default value:\n\n```blade\n<div {{ $attributes->merge(['data-controller' => $attributes->prepends('profile-controller')]) }}>\n    {{ $slot }}\n</div>\n```\n\n---\n\n## Blade Components Slots Slot Attributes\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n#### Slot Attributes\n\nLike Blade components, you may assign additional [attributes](#component-attributes) to slots such as CSS class names:\n\n```xml\n<x-card class=\"shadow-sm\">\n    <x-slot:heading class=\"font-bold\">\n        Heading\n    </x-slot>\n\n    Content\n\n    <x-slot:footer class=\"text-sm\">\n        Footer\n    </x-slot>\n</x-card>\n```\n\nTo interact with slot attributes, you may access the `attributes` property of the slot's variable. For more information on how to interact with attributes, please consult the documentation on [component attributes](#component-attributes):\n\n```blade\n@props([\n    'heading',\n    'footer',\n])\n\n<div {{ $attributes->class(['border']) }}>\n    <h1 {{ $heading->attributes->class(['text-lg']) }}>\n        {{ $heading }}\n    </h1>\n\n    {{ $slot }}\n\n    <footer {{ $footer->attributes->class(['text-gray-700']) }}>\n        {{ $footer }}\n    </footer>\n</div>\n```\n\n_[Truncated]_"}]


Docs confirm the browser assertions and Livewire pagination patterns I need. I’m checking the existing customer admin components and nearby browser suites now so Suite 16 follows the same shape instead of inventing a new test style.


sed -n '1,240p' app/Livewire/Admin/Customers/Index.php


sed -n '1,260p' app/Livewire/Admin/Customers/Show.php


sed -n '1,260p' resources/views/livewire/admin/customers/index.blade.php


sed -n '1,320p' resources/views/livewire/admin/customers/show.blade.php


sed -n '1,260p' tests/Feature/Admin/CustomerManagementTest.php


sed -n '1,260p' tests/Browser/Admin/CollectionManagementTest.php


sed -n '1,240p' app/Livewire/Admin/Customers/Index.php

<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

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

        abort_unless($store instanceof Store, 404);

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

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function customers(): LengthAwarePaginator
    {
        return Customer::withoutGlobalScopes()
            ->withCount('orders')
            ->withSum('orders as total_spent', 'total_amount')
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('name', 'like', $search)
                        ->orWhere('email', 'like', $search);
                });
            })
            ->latest('created_at')
            ->paginate(20);
    }

    public function render(): mixed
    {
        return view('livewire.admin.customers.index', [
            'customers' => $this->customers(),
        ])->layout('layouts.app', [
            'title' => __('Customers'),
        ]);
    }
}


sed -n '1,260p' app/Livewire/Admin/Customers/Show.php

<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

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

        abort_unless($store instanceof Store, 404);

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

        abort_unless($customer instanceof Customer, 404);

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

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

        $this->modal('address-form')->show();
    }

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->orderBy('id')])
            ->where('store_id', $this->storeId)
            ->whereKey($this->customerId)
            ->firstOrFail();
    }

    private function address(int $addressId): CustomerAddress
    {
        return CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->whereKey($addressId)
            ->firstOrFail();
    }

    private function setFirstAddressAsDefault(): void
    {
        $nextAddress = CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->oldest('id')
            ->first();

        if ($nextAddress instanceof CustomerAddress) {
            $nextAddress->forceFill(['is_default' => true])->save();
        }
    }

    private function resetAddressForm(): void
    {
        $this->editingAddressId = null;
        $this->addressLabel = '';
        $this->addressJson = $this->emptyAddressJson();
    }

    /**
     * @return array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    private function emptyAddressJson(): array
    {
        return [
            'first_name' => '',
            'last_name' => '',
            'address1' => '',
            'address2' => '',
            'city' => '',
            'province_code' => '',
            'country' => 'DE',
            'postal_code' => '',
        ];
    }
}


sed -n '1,260p' resources/views/livewire/admin/customers/index.blade.php

        <flux:heading size="xl">Customers</flux:heading>
        <flux:text class="mt-1">Search customer accounts and review order value.</flux:text>

        <flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="Search name or email..." aria-label="Search customers" />

                        Name
                        Email
                        Orders
                        Total spent
                        Created

                    @forelse ($customers as $customer)

getKey() }}">

                                    {{ $customer->name ?: 'Unnamed customer' }}

                            {{ $customer->email }}
                            {{ $customer->orders_count }}

                                <x-storefront.price :amount="(int) ($customer->total_spent ?? 0)" :currency="$storeCurrency" class="justify-end" />

                            {{ $customer->created_at?->format('M j, Y') }}

                    @empty

                                        <flux:icon name="users" class="size-6" />

                                    <flux:heading size="lg">No customers found</flux:heading>
                                    <flux:text>Customer accounts will appear here after registration or checkout.</flux:text>

                    @endforelse

    {{ $customers->links() }}


sed -n '1,320p' resources/views/livewire/admin/customers/show.blade.php

            <flux:heading size="xl">{{ $customer->name ?: 'Unnamed customer' }}</flux:heading>
            <flux:text class="mt-1">{{ $customer->email }}</flux:text>

        <flux:button :href="route('admin.customers.index')" wire:navigate variant="filled" icon="arrow-left">
            Customers
        </flux:button>

                <flux:heading size="lg">Customer info</flux:heading>

Name

{{ $customer->name ?: 'Unnamed customer' }}

Email

{{ $customer->email }}

Created

{{ $customer->created_at?->format('M j, Y') }}

Marketing

                            <flux:badge :color="$customer->marketing_opt_in ? 'green' : 'zinc'">
                                {{ $customer->marketing_opt_in ? 'Opted in' : 'Not opted in' }}
                            </flux:badge>

                    <flux:heading size="lg">Order history</flux:heading>

                                Order
                                Date
                                Status
                                Total

                            @forelse ($orders as $order)
                                @php
                                    $statusColor = match ($order->financial_status->value) {
                                        'paid' => 'green',
                                        'partially_refunded' => 'amber',
                                        'refunded', 'voided' => 'red',
                                        default => 'zinc',
                                    };
                                @endphp

getKey() }}">

                                            {{ $order->order_number }}

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

                                        <flux:badge :color="$statusColor">{{ Str::headline($order->financial_status->value) }}</flux:badge>

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

                            @empty

                                    No orders for this custom…610 chars truncated…

                @forelse ($customer->addresses as $address)
                    @php($addressData = $address->address_json ?? [])

getKey() }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">

{{ $address->label ?: 'Address' }}

                                    @if ($address->is_default)
                                        <flux:badge color="green">Default</flux:badge>
                                    @endif

{{ data_get($addressData, 'address1') }}

                                    @if (data_get($addressData, 'address2'))

{{ data_get($addressData, 'address2') }}

                                    @endif

{{ trim(data_get($addressData, 'postal_code').' '.data_get($addressData, 'city')) }}

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


Output:
Chunk ID: 8e7000
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 431
Output:
<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

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

        abort_unless($store instanceof Store, 404);

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

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function customers(): LengthAwarePaginator
    {
        return Customer::withoutGlobalScopes()
            ->withCount('orders')
            ->withSum('orders as total_spent', 'total_amount')
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('name', 'like', $search)
                        ->orWhere('email', 'like', $search);
                });
            })
            ->latest('created_at')
            ->paginate(20);
    }

    public function render(): mixed
    {
        return view('livewire.admin.customers.index', [
            'customers' => $this->customers(),
        ])->layout('layouts.app', [
            'title' => __('Customers'),
        ]);
    }
}


Output:
Chunk ID: 2f06d9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1610
Output:
<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

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

        abort_unless($store instanceof Store, 404);

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

        abort_unless($customer instanceof Customer, 404);

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

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

        $this->modal('address-form')->show();
    }

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->orderBy('id')])
            ->where('store_id', $this->storeId)
            ->whereKey($this->customerId)
            ->firstOrFail();
    }

    private function address(int $addressId): CustomerAddress
    {
        return CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->whereKey($addressId)
            ->firstOrFail();
    }

    private function setFirstAddressAsDefault(): void
    {
        $nextAddress = CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->oldest('id')
            ->first();

        if ($nextAddress instanceof CustomerAddress) {
            $nextAddress->forceFill(['is_default' => true])->save();
        }
    }

    private function resetAddressForm(): void
    {
        $this->editingAddressId = null;
        $this->addressLabel = '';
        $this->addressJson = $this->emptyAddressJson();
    }

    /**
     * @return array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    private function emptyAddressJson(): array
    {
        return [
            'first_name' => '',
            'last_name' => '',
            'address1' => '',
            'address2' => '',
            'city' => '',
            'province_code' => '',
            'country' => 'DE',
            'postal_code' => '',
        ];
    }
}


Output:
Chunk ID: d29912
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 827
Output:

        <flux:heading size="xl">Customers</flux:heading>
        <flux:text class="mt-1">Search customer accounts and review order value.</flux:text>

        <flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="Search name or email..." aria-label="Search customers" />

                        Name
                        Email
                        Orders
                        Total spent
                        Created

                    @forelse ($customers as $customer)

getKey() }}">

                                    {{ $customer->name ?: 'Unnamed customer' }}

                            {{ $customer->email }}
                            {{ $customer->orders_count }}

                                <x-storefront.price :amount="(int) ($customer->total_spent ?? 0)" :currency="$storeCurrency" class="justify-end" />

                            {{ $customer->created_at?->format('M j, Y') }}

                    @empty

                                        <flux:icon name="users" class="size-6" />

                                    <flux:heading size="lg">No customers found</flux:heading>
                                    <flux:text>Customer accounts will appear here after registration or checkout.</flux:text>

                    @endforelse

    {{ $customers->links() }}


Output:
Chunk ID: ea9fa3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2653
Output:

            <flux:heading size="xl">{{ $customer->name ?: 'Unnamed customer' }}</flux:heading>
            <flux:text class="mt-1">{{ $customer->email }}</flux:text>

        <flux:button :href="route('admin.customers.index')" wire:navigate variant="filled" icon="arrow-left">
            Customers
        </flux:button>

                <flux:heading size="lg">Customer info</flux:heading>

Name

{{ $customer->name ?: 'Unnamed customer' }}

Email

{{ $customer->email }}

Created

{{ $customer->created_at?->format('M j, Y') }}

Marketing

                            <flux:badge :color="$customer->marketing_opt_in ? 'green' : 'zinc'">
                                {{ $customer->marketing_opt_in ? 'Opted in' : 'Not opted in' }}
                            </flux:badge>

                    <flux:heading size="lg">Order history</flux:heading>

                                Order
                                Date
                                Status
                                Total

                            @forelse ($orders as $order)
                                @php
                                    $statusColor = match ($order->financial_status->value) {
                                        'paid' => 'green',
                                        'partially_refunded' => 'amber',
                                        'refunded', 'voided' => 'red',
                                        default => 'zinc',
                                    };
                                @endphp

getKey() }}">

                                            {{ $order->order_number }}

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

                                        <flux:badge :color="$statusColor">{{ Str::headline($order->financial_status->value) }}</flux:badge>

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

                            @empty

                                    No orders for this customer.

                            @endforelse

            {{ $orders->links() }}

                <flux:heading size="lg">Addresses</flux:heading>
                <flux:button wire:click="openAddressForm" variant="primary" icon="plus" size="sm">Add</flux:button>

                @forelse ($customer->addresses as $address)
                    @php($addressData = $address->address_json ?? [])

getKey() }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">

{{ $address->label ?: 'Address' }}

                                    @if ($address->is_default)
                                        <flux:badge color="green">Default</flux:badge>
                                    @endif

{{ data_get($addressData, 'address1') }}

                                    @if (data_get($addressData, 'address2'))

{{ data_get($addressData, 'address2') }}

                                    @endif

{{ trim(data_get($addressData, 'postal_code').' '.data_get($addressData, 'city')) }}

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


sed -n '1,260p' tests/Browser/Admin/CollectionManagementTest.php

<?php

use App\Models\Collection;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function adminCollectionBrowserHost(): array
{
    return ['host' => 'shop.test'];
}

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

function adminCollectionBrowserAuthenticate(mixed $testCase): Store
{
    $store = adminCollectionBrowserStore();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $testCase->actingAs($user);
    $testCase->withSession(['current_store_id' => $store->getKey()]);

    return $store;
}

function adminCollectionOpenCollections(mixed $testCase): mixed
{
    adminCollectionBrowserAuthenticate($testCase);

    return visit('/admin/collections', adminCollectionBrowserHost())
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('Collections')
        ->assertNoJavaScriptErrors();
}

function adminCollectionSave(mixed $page): mixed
{
    return $page
        ->click('button[data-test="collection-save-button"]')
        ->wait(1)
        ->assertSee('Collection saved')
        ->assertNoJavaScriptErrors();
}

test('shows the collection list with seeded collections', function (): void {
    adminCollectionOpenCollections($this)
        ->assertSee('T-Shirts')
        ->assertSee('New Arrivals')
        ->assertNoJavaScriptErrors();
});

test('can create a new collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);
    $page = adminCollectionOpenCollections($this)
        ->click('a[href$="/admin/collections/create"]')
        ->wait(1)
        ->assertPathIs('/admin/collections/create')
        ->assertSee('Add collection')
        ->assertNoJavaScriptErrors();

    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'E2E Test Collection')
        ->fill('input[wire\\:model="handle"]', 'e2e-test-collection')
        ->fill('textarea[wire\\:model="descriptionHtml"]', 'A collection created by the E2E test suite.');

    adminCollectionSave($page)
        ->navigate('/admin/collections')
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('E2E Test Collection')
        ->assertNoJavaScriptErrors();

    $this->assertDatabaseHas('collections', [
        'store_id' => $store->getKey(),
        'title' => 'E2E Test Collection',
        'handle' => 'e2e-test-collection',
        'description_html' => 'A collection created by the E2E test suite.',
    ]);
});

test('can edit a collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);

    $page = adminCollectionOpenCollections($this)
        ->click('a:has-text("T-Shirts")')
        ->wait(1)
        ->assertPathContains('/admin/collections/')
        ->assertValue('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'T-Shirts')
        ->assertNoJavaScriptErrors();

    $page->fill('textarea[wire\\:model="descriptionHtml"]', 'Updated description for T-Shirts collection.');

    adminCollectionSave($page);

    $collection = Collection::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('title', 'T-Shirts')
        ->firstOrFail();

    expect($collection->description_html)->toBe('Updated description for T-Shirts collection.');
});


sed -n '1,260p' tests/Feature/Admin/CustomerManagementTest.php

<?php

use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

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

    return $store;
}

function adminCustomerManagementUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

function adminCustomerManagementCustomer(Store $store, string $email, string $name): Customer
{
    return Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => $email,
        'name' => $name,
    ]);
}

function adminCustomerManagementOrder(Store $store, Customer $customer, string $orderNumber, int $totalAmount): Order
{
    return Order::factory()->paid()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'subtotal_amount' => $totalAmount,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $totalAmount,
        'email' => $customer->email,
    ]);
}

test('admin customer routes require authentication and render store scoped customers', function (): void {
    $store = adminCustomerManagementStore();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementOrder($store, $customer, '#8101', 7000);
    CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    $this->get('/admin/customers')->assertRedirect('/admin/login');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers')
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('70.00 EUR')
        ->assertDontSee('Other Customer');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$customer->getKey())
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('#8101')
        ->assertSee('Home');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$otherCustomer->getKey())
        ->assertNotFound();
});

test('admin customer index filters by name and email', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementCustomer($store, 'bravo@example.test', 'Bravo Example');

    Livewire::actingAs($user)
        ->test(AdminCustomersIndex::class)
        ->assertSee('Jane Example')
        ->assertSee('Bravo Example')
        ->set('search', 'jane')
        ->assertSee('Jane Example')
        ->assertDontSee('Bravo Example')
        ->set('search', 'bravo@example.test')
        ->assertSee('Bravo Example')
        ->assertDontSee('Jane Example');
});

test('admin customer detail manages addresses', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    $home = CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('openAddressForm')
        ->set('addressLabel', 'Office')
        ->set('addressJson.address1', 'Business Street 5')
        ->set('addressJson.city', 'Berlin')
        ->set('addressJson.postal_code', '10115')
        ->set('addressJson.country', 'DE')
        ->call('saveAddress')
        ->assertHasNoErrors();

    $office = CustomerAddress::query()
        ->where('customer_id', $customer->getKey())
        ->where('label', 'Office')
        ->firstOrFail();

    expect($office->is_default)->toBeFalse();

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('setDefaultAddress', $office->getKey())
        ->assertHasNoErrors()
        ->call('openAddressForm', $office->getKey())
        ->set('addressLabel', 'HQ')
        ->call('saveAddress')
        ->assertHasNoErrors()
        ->call('deleteAddress', $home->getKey())
        ->assertHasNoErrors();

    expect($office->refresh()->is_default)->toBeTrue()
        ->and($office->label)->toBe('HQ')
        ->and(CustomerAddress::query()->whereKey($home->getKey())->exists())->toBeFalse();
});

test('admin customer detail rejects customers from another store', function (): void {
    $store = adminCustomerManagementStore();
    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

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

    Livewire::actingAs(adminCustomerManagementUser())
        ->test(AdminCustomerShow::class, ['customer' => $otherCustomer])
        ->assertStatus(404);
});


Output:
Chunk ID: 8c8471
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1460
Output:
<?php

use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

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

    return $store;
}

function adminCustomerManagementUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

function adminCustomerManagementCustomer(Store $store, string $email, string $name): Customer
{
    return Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => $email,
        'name' => $name,
    ]);
}

function adminCustomerManagementOrder(Store $store, Customer $customer, string $orderNumber, int $totalAmount): Order
{
    return Order::factory()->paid()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'subtotal_amount' => $totalAmount,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $totalAmount,
        'email' => $customer->email,
    ]);
}

test('admin customer routes require authentication and render store scoped customers', function (): void {
    $store = adminCustomerManagementStore();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementOrder($store, $customer, '#8101', 7000);
    CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    $this->get('/admin/customers')->assertRedirect('/admin/login');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers')
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('70.00 EUR')
        ->assertDontSee('Other Customer');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$customer->getKey())
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('#8101')
        ->assertSee('Home');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$otherCustomer->getKey())
        ->assertNotFound();
});

test('admin customer index filters by name and email', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementCustomer($store, 'bravo@example.test', 'Bravo Example');

    Livewire::actingAs($user)
        ->test(AdminCustomersIndex::class)
        ->assertSee('Jane Example')
        ->assertSee('Bravo Example')
        ->set('search', 'jane')
        ->assertSee('Jane Example')
        ->assertDontSee('Bravo Example')
        ->set('search', 'bravo@example.test')
        ->assertSee('Bravo Example')
        ->assertDontSee('Jane Example');
});

test('admin customer detail manages addresses', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    $home = CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('openAddressForm')
        ->set('addressLabel', 'Office')
        ->set('addressJson.address1', 'Business Street 5')
        ->set('addressJson.city', 'Berlin')
        ->set('addressJson.postal_code', '10115')
        ->set('addressJson.country', 'DE')
        ->call('saveAddress')
        ->assertHasNoErrors();

    $office = CustomerAddress::query()
        ->where('customer_id', $customer->getKey())
        ->where('label', 'Office')
        ->firstOrFail();

    expect($office->is_default)->toBeFalse();

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('setDefaultAddress', $office->getKey())
        ->assertHasNoErrors()
        ->call('openAddressForm', $office->getKey())
        ->set('addressLabel', 'HQ')
        ->call('saveAddress')
        ->assertHasNoErrors()
        ->call('deleteAddress', $home->getKey())
        ->assertHasNoErrors();

    expect($office->refresh()->is_default)->toBeTrue()
        ->and($office->label)->toBe('HQ')
        ->and(CustomerAddress::query()->whereKey($home->getKey())->exists())->toBeFalse();
});

test('admin customer detail rejects customers from another store', function (): void {
    $store = adminCustomerManagementStore();
    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

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

    Livewire::actingAs(adminCustomerManagementUser())
        ->test(AdminCustomerShow::class, ['customer' => $otherCustomer])
        ->assertStatus(404);
});


Output:
Chunk ID: 34be73
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 916
Output:
<?php

use App\Models\Collection;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function adminCollectionBrowserHost(): array
{
    return ['host' => 'shop.test'];
}

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

function adminCollectionBrowserAuthenticate(mixed $testCase): Store
{
    $store = adminCollectionBrowserStore();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $testCase->actingAs($user);
    $testCase->withSession(['current_store_id' => $store->getKey()]);

    return $store;
}

function adminCollectionOpenCollections(mixed $testCase): mixed
{
    adminCollectionBrowserAuthenticate($testCase);

    return visit('/admin/collections', adminCollectionBrowserHost())
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('Collections')
        ->assertNoJavaScriptErrors();
}

function adminCollectionSave(mixed $page): mixed
{
    return $page
        ->click('button[data-test="collection-save-button"]')
        ->wait(1)
        ->assertSee('Collection saved')
        ->assertNoJavaScriptErrors();
}

test('shows the collection list with seeded collections', function (): void {
    adminCollectionOpenCollections($this)
        ->assertSee('T-Shirts')
        ->assertSee('New Arrivals')
        ->assertNoJavaScriptErrors();
});

test('can create a new collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);
    $page = adminCollectionOpenCollections($this)
        ->click('a[href$="/admin/collections/create"]')
        ->wait(1)
        ->assertPathIs('/admin/collections/create')
        ->assertSee('Add collection')
        ->assertNoJavaScriptErrors();

    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'E2E Test Collection')
        ->fill('input[wire\\:model="handle"]', 'e2e-test-collection')
        ->fill('textarea[wire\\:model="descriptionHtml"]', 'A collection created by the E2E test suite.');

    adminCollectionSave($page)
        ->navigate('/admin/collections')
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('E2E Test Collection')
        ->assertNoJavaScriptErrors();

    $this->assertDatabaseHas('collections', [
        'store_id' => $store->getKey(),
        'title' => 'E2E Test Collection',
        'handle' => 'e2e-test-collection',
        'description_html' => 'A collection created by the E2E test suite.',
    ]);
});

test('can edit a collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);

    $page = adminCollectionOpenCollections($this)
        ->click('a:has-text("T-Shirts")')
        ->wait(1)
        ->assertPathContains('/admin/collections/')
        ->assertValue('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'T-Shirts')
        ->assertNoJavaScriptErrors();

    $page->fill('textarea[wire\\:model="descriptionHtml"]', 'Updated description for T-Shirts collection.');

    adminCollectionSave($page);

    $collection = Collection::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('title', 'T-Shirts')
        ->firstOrFail();

    expect($collection->description_html)->toBe('Updated description for T-Shirts collection.');
});


The customer pages already have the needed list, detail, order history, and address sections. The only likely code change will be a small browser suite plus any minimal selector hardening if the browser can’t target the existing links reliably.


sed -n '1,280p' tests/Browser/Admin/OrderManagementTest.php


sed -n '1,220p' database/factories/OrderFactory.php


sed -n '1,220p' database/factories/OrderLineFactory.php


sed -n '1,220p' database/factories/PaymentFactory.php


sed -n '1,220p' database/factories/CustomerAddressFactory.php


rg -n "customer@acme|John Doe|Classic Cotton|classic-cotton" database app tests specs -S


sed -n '1,220p' database/factories/OrderFactory.php

<?php

namespace Database\Factories;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $subtotal = fake()->numberBetween(2500, 25000);
        $shipping = fake()->randomElement([0, 499, 799]);
        $tax = 0;

        return [
            'store_id' => Store::factory(),
            'checkout_id' => null,
            'customer_id' => null,
            'order_number' => '#'.fake()->unique()->numberBetween(1001, 9999),
            'payment_method' => PaymentMethod::CreditCard,
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Unfulfilled,
            'currency' => 'EUR',
            'subtotal_amount' => $subtotal,
            'discount_amount' => 0,
            'shipping_amount' => $shipping,
            'tax_amount' => $tax,
            'total_amount' => $subtotal + $shipping + $tax,
            'email' => fake()->safeEmail(),
            'billing_address_json' => $this->address(),
            'shipping_address_json' => $this->address(),
            'placed_at' => now(),
        ];
    }

    public function forCheckout(?Checkout $checkout = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'checkout_id' => $checkout?->getKey() ?? Checkout::factory(),
        ]);
    }

    public function forCustomer(?Customer $customer = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'customer_id' => $customer?->getKey() ?? Customer::factory(),
        ]);
    }

    public function paid(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
        ]);
    }

    public function pending(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function creditCard(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::CreditCard,
        ]);
    }

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::BankTransfer,
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function cancelled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Cancelled,
            'financial_status' => FinancialStatus::Voided,
        ]);
    }

    public function partiallyFulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'fulfillment_status' => FulfillmentStatus::Partial,
        ]);
    }

    public function fulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Fulfilled,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Fulfilled,
        ]);
    }

    public function refunded(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Refunded,
            'financial_status' => FinancialStatus::Refunded,
        ]);
    }

    /**
     * @return array<string, string|null>
     */
    private function address(): array
    {
        return [
            'first_name' => fake()->firstName(),
            'last_name' => fake()->lastName(),
            'address1' => fake()->streetAddress(),
            'address2' => null,
            'city' => fake()->city(),
            'province_code' => null,
            'country' => 'DE',
            'postal_code' => fake()->postcode(),
        ];
    }
}


sed -n '1,280p' tests/Browser/Admin/OrderManagementTest.php

<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Models\Customer;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function adminOrderHost(): array
{
    return ['host' => 'shop.test'];
}

function adminOrderAuthenticate(mixed $testCase): void
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $testCase->actingAs($user);
    $testCase->withSession(['current_store_id' => $store->getKey()]);
}

/**
 * @return array{store: Store, paid: Order, fulfilled: Order, bank: Order}
 */
function adminOrderFixtures(): array
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $customer = Customer::query()->where('email', 'customer@acme.test')->firstOrFail();
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'classic-cotton-t-shirt')
        ->firstOrFail();
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $paid = adminOrderCreateOrder(
        store: $store,
        customer: $customer,
        product: $product,
        variant: $variant,
        orderNumber: '#1001',
        paymentMethod: PaymentMethod::CreditCard,
        status: OrderStatus::Paid,
        financialStatus: FinancialStatus::Paid,
        fulfillmentStatus: FulfillmentStatus::Unfulfilled,
        placedAt: now()->subDays(3),
    );

    $fulfilled = adminOrderCreateOrder(
        store: $store,
        customer: $customer,
        product: $product,
        variant: $variant,
        orderNumber: '#1002',
        paymentMethod: PaymentMethod::CreditCard,
        status: OrderStatus::Fulfilled,
        financialStatus: FinancialStatus::Paid,
        fulfillmentStatus: FulfillmentStatus::Fulfilled,
        placedAt: now()->subDays(2),
    );

    adminOrderCreateDeliveredFulfillment($fulfilled);

    $bank = adminOrderCreateOrder(
        store: $store,
        customer: $customer,
        product: $product,
        variant: $variant,
        orderNumber: '#1005',
        paymentMethod: PaymentMethod::BankTransfer,
        status: OrderStatus::Pending,
        financialStatus: FinancialStatus::Pending,
        fulfillmentStatus: FulfillmentStatus::Unfulfilled,
        placedAt: now()->subDay(),
    );

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

    return [
        'store' => $store,
        'paid' => $paid,
        'fulfilled' => $fulfilled,
        'bank' => $bank,
    ];
}

function adminOrderCreateOrder(
    Store $store,
    Customer $customer,
    Product $product,
    ProductVariant $variant,
    string $orderNumber,
    PaymentMethod $paymentMethod,
    OrderStatus $status,
    FinancialStatus $financialStatus,
    FulfillmentStatus $fulfillmentStatus,
    mixed $placedAt,
): Order {
    $subtotal = $variant->price_amount;
    $shipping = 499;
    $tax = 0;
    $total = $subtotal + $shipping + $tax;

    $order = Order::factory()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'payment_method' => $paymentMethod,
        'status' => $status,
        'financial_status' => $financialStatus,
        'fulfillment_status' => $fulfillmentStatus,
        'currency' => $store->default_currency,
        'subtotal_amount' => $subtotal,
        'discount_amount' => 0,
        'shipping_amount' => $shipping,
        'tax_amount' => $tax,
        'total_amount' => $total,
        'email' => $customer->email,
        'placed_at' => $placedAt,
    ]);

    OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Classic Cotton T-Shirt',
        'sku_snapshot' => $variant->sku,
        'quantity' => 1,
        'unit_price_amount' => $variant->price_amount,
        'total_amount' => $variant->price_amount,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'method' => $paymentMethod,
        'status' => $financialStatus === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $store->default_currency,
    ]);

    return $order->refresh();
}

function adminOrderCreateDeliveredFulfillment(Order $order): void
{
    $line = $order->lines()->firstOrFail();
    $fulfillment = Fulfillment::query()->create([
        'order_id' => $order->getKey(),
        'status' => FulfillmentShipmentStatus::Delivered,
        'tracking_company' => 'DHL',
        'tracking_number' => 'DHL1234567890',
        'shipped_at' => now()->subDay(),
        'delivered_at' => now(),
    ]);

    $fulfillment->lines()->create([
        'order_line_id' => $line->getKey(),
        'quantity' => $line->quantity,
    ]);
}

function adminOrderOpenOrders(mixed $testCase): mixed
{
    adminOrderFixtures();
    adminOrderAuthenticate($testCase);

    return visit('/admin/orders', adminOrderHost())
        ->wait(1)
        ->assertPathIs('/admin/orders')
        ->assertSee('Orders')
        ->assertNoJavaScriptErrors();
}

function adminOrderOpenOrder(mixed $testCase, string $orderNumber): mixed
{
    return adminOrderOpenOrders($testCase)
        ->assertSee($orderNumber)
        ->click("a:has-text(\"{$orderNumber}\")")
        ->wait(1)
        ->assertSee($orderNumber)
        ->assertNoJavaScriptErrors();
}

function adminOrderCreateFulfillmentInBrowser(mixed $page): mixed
{
    return $page
        ->click('button[data-test="fulfillment-modal-button"]')
        ->wait(1)
        ->fill('input[wire\\:model="trackingCompany"]', 'DHL')
        ->fill('input[wire\\:model="trackingNumber"]', 'DHL123456789')
        ->click('button[data-test="fulfillment-submit-button"]')
        ->wait(1)
        ->assertSee('Fulfillment created')
        ->assertSee('DHL')
        ->assertSee('DHL123456789')
        ->assertNoJavaScriptErrors();
}

test('shows the order list with seeded orders', function (): void {
    adminOrderOpenOrders($this)
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('can filter orders by status', function (): void {
    adminOrderOpenOrders($this)
        ->select('select[wire\\:model\\.live="financialStatusFilter"]', 'paid')
        ->wait(1)
        ->assertSee('#1001')
        ->assertDontSee('#1005')
        ->assertNoJavaScriptErrors()
        ->select('select[wire\\:model\\.live="fulfillmentStatusFilter"]', 'fulfilled')
        ->wait(1)
        ->assertSee('#1002')
        ->assertNoJavaScriptErrors()
        ->select('select[wire\\:model\\.live="financialStatusFilter"]', 'all')
        ->select('select[wire\\:model\\.live="fulfillmentStatusFilter"]', 'all')
        ->wait(1)
        ->assertSee('#1001')
        ->assertSee('#1005')
        ->assertNoJavaScriptErrors();
});

test('shows order detail with line items and totals', function (): void {
    adminOrderOpenOrder($this, '#1001')
        ->assertSee('Paid')
        ->assertSee('Unfulfilled')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Subtotal')
        ->assertSee('Shipping')
        ->assertSee('Tax')
        ->assertSee('Total')
        ->assertNoJavaScriptErrors();
});

test('shows order timeline events', function (): void {
    adminOrderOpenOrder($this, '#1001')
        ->assertSee('Timeline')
        ->assertSee('Order placed')
        ->assertNoJavaScriptErrors();
});

test('can create a fulfillment', function (): void {
    adminOrderCreateFulfillmentInBrowser(adminOrderOpenOrder($this, '#1001'));
});

test('can process a refund', function (): void {


sed -n '1,220p' database/factories/OrderLineFactory.php

<?php

namespace Database\Factories;

use App\Models\Order;
use App\Models\ProductVariant;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OrderLine>
 */
class OrderLineFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $quantity = fake()->numberBetween(1, 3);
        $unitPrice = fake()->numberBetween(1000, 10000);

        return [
            'order_id' => Order::factory(),
            'product_id' => null,
            'variant_id' => ProductVariant::factory(),
            'title_snapshot' => fake()->words(3, true),
            'sku_snapshot' => 'SKU-'.fake()->unique()->numerify('####'),
            'quantity' => $quantity,
            'unit_price_amount' => $unitPrice,
            'total_amount' => $unitPrice * $quantity,
            'tax_lines_json' => [],
            'discount_allocations_json' => [],
        ];
    }
}


sed -n '1,220p' database/factories/PaymentFactory.php

<?php

namespace Database\Factories;

use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Models\Order;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Payment>
 */
class PaymentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'order_id' => Order::factory(),
            'provider' => 'mock',
            'method' => PaymentMethod::CreditCard,
            'provider_payment_id' => 'mock_payment_'.fake()->unique()->lexify('????????????????'),
            'status' => PaymentStatus::Captured,
            'amount' => fake()->numberBetween(2500, 25000),
            'currency' => 'EUR',
            'raw_json_encrypted' => [
                'success' => true,
                'status' => PaymentStatus::Captured->value,
            ],
        ];
    }

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

    public function failed(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => PaymentStatus::Failed,
            'raw_json_encrypted' => [
                'success' => false,
                'status' => PaymentStatus::Failed->value,
                'error_code' => 'card_declined',
            ],
        ]);
    }

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

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'method' => PaymentMethod::BankTransfer,
            'status' => PaymentStatus::Pending,
            'provider_payment_id' => 'mock_bank_'.fake()->unique()->lexify('????????????????'),
        ]);
    }
}


sed -n '1,220p' database/factories/CustomerAddressFactory.php

<?php

namespace Database\Factories;

use App\Models\Customer;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CustomerAddress>
 */
class CustomerAddressFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'customer_id' => Customer::factory(),
            'label' => fake()->randomElement(['Shipping', 'Billing', 'Home', 'Office']),
            'address_json' => [
                'first_name' => fake()->firstName(),
                'last_name' => fake()->lastName(),
                'address1' => fake()->streetAddress(),
                'address2' => null,
                'city' => fake()->city(),
                'province_code' => null,
                'country' => 'DE',
                'postal_code' => fake()->postcode(),
            ],
            'is_default' => false,
        ];
    }

    public function default(): static
    {
        return $this->state(fn (array $attributes): array => [
            'is_default' => true,
        ]);
    }
}