# Search Results

## Verification Introduction Database Preparation
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Database Preparation

Next, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

	<button type="submit">Save</button>
</form>
```

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Wire Transition Browser support
Wire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition

## Browser support

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

> [!warning] Firefox has limited support
> Firefox 144+ supports basic view transitions, but does not support transition types.

[View browser support on caniuse.com →](https://caniuse.com/view-transitions)

---

## Testing Browser testing
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## Browser testing

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

### Installing browser testing

First, install the Pest browser plugin:

```shell
composer require pestphp/pest-plugin-browser --dev
```

Next, install Playwright via npm:

```shell
npm install playwright@latest
npx playwright install
```

For complete browser testing documentation, see the [Pest browser testing guide](https://pestphp.com/docs/browser-testing).

### Writing browser tests

Instead of using `Livewire::test()`, you can use `Livewire::visit()` to test your component in a real browser:

```php
it('can create a new post', function () {
    Livewire::visit('post.create')
        ->type('[wire\:model="title"]', 'My first post')
        ->type('[wire\:model="content"]', 'This is the content')
        ->press('Save')
        ->assertSee('Post created successfully');
});
```

Browser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.

For a complete list of available browser testing assertions, see the [Pest browser testing assertions](https://pestphp.com/docs/browser-testing#content-available-assertions).

> [!info] When to use browser tests
> Use browser tests for critical user flows and complex interactions. For most component testing, the standard `Livewire::test()` approach is faster and sufficient.

---

## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Database Constructor Signature Changes

**Likelihood Of Impact: Very Low**

In Laravel 12, several low-level database classes now require an `Illuminate\Database\Connection` instance to be provided via their constructors.

**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**

`Illuminate\Database\Schema\Blueprint`

The constructor of the `Illuminate\Database\Schema\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.

`Illuminate\Database\Grammar`

The constructor of the `Illuminate\Database\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:

```php
// Laravel <= 11.x
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

// Laravel >= 12.x
$grammar = new MySqlGrammar($connection);
````

In addition, the following APIs have been removed or deprecated:

- The `Blueprint::getPrefix()` method is deprecated.
- The `Connection::withTablePrefix()` method has been removed.
- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.
- The `Grammar::setConnection()` method has been removed.

When working with table prefixes, you should now retrieve them directly from the database connection:

```php
$prefix = $connection->getTablePrefix();
```

If you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.

---

## Understanding Nesting Performance implications
Understanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting

## Performance implications

Livewire's independent component architecture can have both positive and negative implications for your application.

An advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.

However, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.

For example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be "reactive". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.

Livewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).

Armed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.

---

## Attribute Validate Example: Contact form
Attribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate

## Example: Contact form

Here's a complete contact form with validation:

```php
<?php // resources/views/pages/⚡contact.blade.php

use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Mail;

new class extends Component {
    #[Validate('required|min:2', as: 'name')]
    public $name = '';

    #[Validate('required|email')]
    public $email = '';

    #[Validate('required')]
    public $subject = '';

    #[Validate('required|min:10', as: 'message')]
    public $message = '';

    public function submit()
    {
        $validated = $this->validate();

        Mail::to('support@example.com')->send(new ContactMessage($validated));

        session()->flash('success', 'Message sent successfully!');

        $this->reset();
    }
};
?>

<div>
    @if (session('success'))
        <div class="alert">{{ session('success') }}</div>
    @endif

    <form wire:submit="submit">
        <div>
            <input type="text" wire:model.live.blur="name" placeholder="Your name">
            @error('name') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="email" wire:model.live.blur="email" placeholder="Your email">
            @error('email') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="text" wire:model.live.blur="subject" placeholder="Subject">
            @error('subject') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <textarea wire:model.live.blur="message" placeholder="Your message"></textarea>
            @error('message') <span class="error">{{ $message }}</span> @enderror
        </div>

        <button type="submit">Send Message</button>
    </form>
</div>
```

Users get immediate feedback as they fill out the form, with friendly field names and helpful error messages.

---

## Browser Testing check
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### check

The `check` method checks the given checkbox:

```php
$page->check('terms');
$page->check('color', 'blue'); // For checkbox with specific value
```

---

## Database Database Transactions
Database | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database.md

## Database Transactions

You may use the `transaction` method provided by the `DB` facade to run a set of operations within a database transaction. If an exception is thrown within the transaction closure, the transaction will automatically be rolled back and the exception is re-thrown. If the closure executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using the `transaction` method:

```php
use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    DB::update('update users set votes = 1');

    DB::delete('delete from posts');
});
```

---

## Forms See also
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## See also

- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback
- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties
- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms
- **[Actions](/docs/4.x/actions)** — Process form submissions with actions
- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission

---

## Lifecycle Hooks Using hooks inside a form object
Lifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks

## Using hooks inside a form object

Form objects in Livewire support property update hooks. These hooks work similarly to [component update hooks](#update), letting you perform actions when properties in the form object change.

Below is an example of a component using a `PostForm` form object:

```php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;

new class extends Component {
    public PostForm $form;

    // ...
};
```

Here's the `PostForm` form object containing all the available hooks:

```php
namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    public $title = '';

    public $tags = [];

    public function updating($property, $value)
    {
        // ...
    }

    public function updated($property, $value)
    {
        // ...
    }

    public function updatingTitle($value)
    {
        // ...
    }

    public function updatedTitle($value)
    {
        // ...
    }

    public function updatingTags($value, $key)
    {
        // ...
    }

    public function updatedTags($value, $key)
    {
        // ...
    }

    // ...
}
```

---

## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Multi-Schema Database Inspecting

**Likelihood Of Impact: Low**

The `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:

```php
// All tables on all schemas...
$tables = Schema::getTables();

// All tables on the 'main' schema...
$tables = Schema::getTables(schema: 'main');

// All tables on the 'main' and 'blog' schemas...
$tables = Schema::getTables(schema: ['main', 'blog']);
```

The `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:

```php
$tables = Schema::getTableListing();
// ['main.migrations', 'main.users', 'blog.posts']

$tables = Schema::getTableListing(schema: 'main');
// ['main.migrations', 'main.users']

$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);
// ['migrations', 'users']
```

The `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.

---

## Styles Browser support
Styles | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/styles

## Browser support

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

---

## Installation Components not updating or errors in browser console
Installation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/installation

## Components not updating or errors in browser console

**Check the following:**
- Ensure `@livewireStyles` is in the `<head>` of your layout
- Ensure `@livewireScripts` is before `</body>` in your layout
- Check your browser's developer console for JavaScript errors
- Verify you're running a supported PHP version (8.1+) and Laravel version (10+)
- Clear your application cache: `php artisan cache:clear`

If issues persist, check the [troubleshooting documentation](/docs/4.x/troubleshooting) for more detailed debugging steps.

_[Truncated]_


Output:
Wall time: 4.1281 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Verification Introduction Database Preparation\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Database Preparation\n\nNext, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.\n\n---\n\n## Validation Form objects\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Form objects\n\nAs more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.\n\nBelow is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:\n\n```php\n<?php\n\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    #[Validate('required|min:3')]\n\tpublic $title = '';\n\n    #[Validate('required|min:3')]\n    public $content = '';\n}\n```\n\nThe `PostForm` above can now be defined as a property on the `CreatePost` component:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse App\\Livewire\\Forms\\PostForm;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nclass CreatePost extends Component\n{\n    public PostForm $form;\n\n    public function save()\n    {\n\t\tPost::create(\n    \t\t$this->form->all()\n    \t);\n\n\t\treturn redirect()->to('/posts');\n    }\n\n    // ...\n}\n```\n\nAs you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.\n\nAlso, when referencing the property names in the template, you must prepend `form.` to each instance:\n\n```blade\n<form wire:submit=\"save\">\n\t<input type=\"text\" wire:model=\"form.title\">\n    <div>@error('form.title') {{ $message }} @enderror</div>\n\n\t<textarea wire:model=\"form.content\"></textarea>\n    <div>@error('form.content') {{ $message }} @enderror</div>\n\n\t<button type=\"submit\">Save</button>\n</form>\n```\n\nWhen using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:\n\n```php\npublic function save()\n{\n    Post::create(\n        $this->form->validate()\n    );\n\n    return redirect()->to('/posts');\n}\n```\n\nForm objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).\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## 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 Database Database Constructor Signature Changes\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Database Constructor Signature Changes\n\n**Likelihood Of Impact: Very Low**\n\nIn Laravel 12, several low-level database classes now require an `Illuminate\\Database\\Connection` instance to be provided via their constructors.\n\n**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**\n\n`Illuminate\\Database\\Schema\\Blueprint`\n\nThe constructor of the `Illuminate\\Database\\Schema\\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.\n\n`Illuminate\\Database\\Grammar`\n\nThe constructor of the `Illuminate\\Database\\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:\n\n```php\n// Laravel <= 11.x\n$grammar = new MySqlGrammar;\n$grammar->setConnection($connection);\n\n// Laravel >= 12.x\n$grammar = new MySqlGrammar($connection);\n````\n\nIn addition, the following APIs have been removed or deprecated:\n\n
\n\n- The `Blueprint::getPrefix()` method is deprecated.\n- The `Connection::withTablePrefix()` method has been removed.\n- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.\n- The `Grammar::setConnection()` method has been removed.\n\n
\n\nWhen working with table prefixes, you should now retrieve them directly from the database connection:\n\n```php\n$prefix = $connection->getTablePrefix();\n```\n\nIf you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.\n\n---\n\n## Understanding Nesting Performance implications\nUnderstanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting\n\n## Performance implications\n\nLivewire's independent component architecture can have both positive and negative implications for your application.\n\nAn advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.\n\nHowever, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.\n\nFor example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be \"reactive\". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.\n\nLivewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).\n\nArmed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.\n\n---\n\n## Attribute Validate Example: Contact form\nAttribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate\n\n## Example: Contact form\n\nHere's a complete contact form with validation:\n\n```php\n<?php // resources/views/pages/⚡contact.blade.php\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Component;\nuse App\\Mail\\ContactMessage;\nuse Illuminate\\Support\\Facades\\Mail;\n\nnew class extends Component {\n    #[Validate('required|min:2', as: 'name')]\n    public $name = '';\n\n    #[Validate('required|email')]\n    public $email = '';\n\n    #[Validate('required')]\n    public $subject = '';\n\n    #[Validate('required|min:10', as: 'message')]\n    public $message = '';\n\n    public function submit()\n    {\n        $validated = $this->validate();\n\n        Mail::to('support@example.com')->send(new ContactMessage($validated));\n\n        session()->flash('success', 'Message sent successfully!');\n\n        $this->reset();\n    }\n};\n?>\n\n<div>\n    @if (session('success'))\n        <div class=\"alert\">{{ session('success') }}</div>\n    @endif\n\n    <form wire:submit=\"submit\">\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"name\" placeholder=\"Your name\">\n            @error('name') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"email\" wire:model.live.blur=\"email\" placeholder=\"Your email\">\n            @error('email') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"subject\" placeholder=\"Subject\">\n            @error('subject') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <textarea wire:model.live.blur=\"message\" placeholder=\"Your message\"></textarea>\n            @error('message') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <button type=\"submit\">Send Message</button>\n    </form>\n</div>\n```\n\nUsers get immediate feedback as they fill out the form, with friendly field names and helpful error messages.\n\n---\n\n## Browser Testing check\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### check\n\nThe `check` method checks the given checkbox:\n\n```php\n$page->check('terms');\n$page->check('color', 'blue'); // For checkbox with specific value\n```\n\n---\n\n## Database Database Transactions\nDatabase | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database.md\n\n\n## Database Transactions\n\nYou may use the `transaction` method provided by the `DB` facade to run a set of operations within a database transaction. If an exception is thrown within the transaction closure, the transaction will automatically be rolled back and the exception is re-thrown. If the closure executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using the `transaction` method:\n\n```php\nuse Illuminate\\Support\\Facades\\DB;\n\nDB::transaction(function () {\n    DB::update('update users set votes = 1');\n\n    DB::delete('delete from posts');\n});\n```\n\n---\n\n## Forms See also\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## See also\n\n- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback\n- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties\n- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms\n- **[Actions](/docs/4.x/actions)** — Process form submissions with actions\n- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission\n\n---\n\n## Lifecycle Hooks Using hooks inside a form object\nLifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks\n\n## Using hooks inside a form object\n\nForm objects in Livewire support property update hooks. These hooks work similarly to [component update hooks](#update), letting you perform actions when properties in the form object change.\n\nBelow is an example of a component using a `PostForm` form object:\n\n```php\n<?php // resources/views/components/post/⚡create.blade.php\n\nuse Livewire\\Component;\n\nnew class extends Component {\n    public PostForm $form;\n\n    // ...\n};\n```\n\nHere's the `PostForm` form object containing all the available hooks:\n\n```php\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    public $title = '';\n\n    public $tags = [];\n\n    public function updating($property, $value)\n    {\n        // ...\n    }\n\n    public function updated($property, $value)\n    {\n        // ...\n    }\n\n    public function updatingTitle($value)\n    {\n        // ...\n    }\n\n    public function updatedTitle($value)\n    {\n        // ...\n    }\n\n    public function updatingTags($value, $key)\n    {\n        // ...\n    }\n\n    public function updatedTags($value, $key)\n    {\n        // ...\n    }\n\n    // ...\n}\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Multi-Schema Database Inspecting\n\n**Likelihood Of Impact: Low**\n\nThe `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:\n\n```php\n// All tables on all schemas...\n$tables = Schema::getTables();\n\n// All tables on the 'main' schema...\n$tables = Schema::getTables(schema: 'main');\n\n// All tables on the 'main' and 'blog' schemas...\n$tables = Schema::getTables(schema: ['main', 'blog']);\n```\n\nThe `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:\n\n```php\n$tables = Schema::getTableListing();\n// ['main.migrations', 'main.users', 'blog.posts']\n\n$tables = Schema::getTableListing(schema: 'main');\n// ['main.migrations', 'main.users']\n\n$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);\n// ['migrations', 'users']\n```\n\nThe `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.\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## 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_[Truncated]_"}]


sed -n '2320,2520p' specs/08-PLAYWRIGHT-E2E-PLAN.md

**File:** `tests/Browser/Storefront/InventoryTest.php`

**Purpose:** Verify that inventory policies are correctly enforced on the storefront - deny-policy blocks purchasing when out of stock, continue-policy allows backorders.

### Inventory Policy Decision Matrix

| Stock Level | Policy | Storefront Display | Add-to-Cart |
|-------------|--------|--------------------|-------------|
| > 0 | `deny` | "Add to cart" button enabled | Allowed |
| > 0 | `continue` | "Add to cart" button enabled | Allowed |
| 0 | `deny` | "Sold out" text, button disabled | Blocked |
| 0 | `continue` | "Available on backorder" text, button enabled | Allowed |

---

### Test 11.1: Blocks add-to-cart for out-of-stock deny-policy product

**Preconditions:** Product #17 (0 stock, inventory policy = deny)

**Steps:**
1. Navigate to the product page for Product #17 (use actual handle from seeder)

**Expected Results:**
- Page displays "Sold out"
- "Add to cart" button is disabled or hidden (not clickable)
- No JavaScript errors

---

### Test 11.2: Allows add-to-cart for out-of-stock continue-policy product

**Preconditions:** Product #18 (0 stock, inventory policy = continue)

**Steps:**
1. Navigate to the product page for Product #18 (use actual handle from seeder)
2. Verify "Available on backorder" is displayed
3. Click "Add to cart"
4. Navigate to `/cart`

**Expected Results:**
- "Available on backorder" is visible on the product page
- Add to cart succeeds
- Cart page shows the product
- No JavaScript errors

---

### Test 11.3: Shows correct stock status for in-stock product

**Preconditions:** Product #1 (in stock)

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Add to cart" button (enabled)
- "Sold out" is NOT displayed
- "Available on backorder" is NOT displayed
- No JavaScript errors

---

### Test 11.4: Prevents adding more than available stock for deny-policy product

**Preconditions:** A product with limited stock (deny policy)

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Attempt to increase quantity beyond the available stock level

**Expected Results:**
- System prevents exceeding available inventory (either caps quantity or shows error)
- No JavaScript errors

**Total: 4 tests**

---

## Suite 12: Tenant Isolation

**File:** `tests/Browser/Storefront/TenantIsolationTest.php`

**Purpose:** Verify that multi-store tenant isolation is enforced - data from one store never leaks into another store's storefront or admin panel.

### Isolation Requirements

```mermaid
flowchart LR
    subgraph Store1[Store 1 - Acme Fashion]
        S1Products[Fashion Products]
        S1Orders[Fashion Orders]
        S1Customers[Fashion Customers]
    end
    subgraph Store2[Store 2 - Different Store]
        S2Products[Other Products]
        S2Orders[Other Orders]
        S2Customers[Other Customers]
    end
    S1Products -.-x S2Products
    S1Orders -.-x S2Orders
    S1Customers -.-x S2Customers
```

Dashed X lines indicate these must NEVER cross. No data from Store 2 should ever appear when browsing Store 1, and vice versa.

---

### Test 12.1: Store 1 only shows Store 1 products

**Preconditions:** Seeded Store 1 products

**Steps:**
1. Navigate to `/` (resolved to Store 1 via acme-fashion.test domain)

**Expected Results:**
- Page displays "Acme Fashion"
- Page displays "Classic Cotton T-Shirt"
- Products from other stores (e.g., electronics from Store 2) are absent
- No JavaScript errors

---

### Test 12.2: Store 1 collections only contain Store 1 products

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- Only Store 1 products appear in the collection grid
- No JavaScript errors

---

### Test 12.3: Admin cannot access other store data

**Preconditions:** Logged in as admin@acme.test (Store 1 owner)

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin (email: `admin@acme.test`, password: `password`)
3. Click "Products" in the sidebar
4. Review the product list
5. Click "Orders" in the sidebar
6. Review the order list

**Expected Results:**
- All products listed belong to Store 1 only
- All orders listed belong to Store 1 only
- No cross-store data is visible
- No JavaScript errors

---

### Test 12.4: Search only returns current store products

**Preconditions:** Seeded Store 1 products

**Steps:**
1. Navigate to `/search?q=product`

**Expected Results:**
- Only Store 1 products appear in search results
- No cross-store data leakage
- No JavaScript errors

---

### Test 12.5: Customer accounts are scoped to their store

**Preconditions:** customer@acme.test seeded for Store 1

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Click "Orders"

**Expected Results:**
- Customer only sees Store 1 orders (#1001, #1002, #1004)
- No JavaScript errors

**Total: 5 tests**

---

## Suite 13: Responsive / Mobile

**File:** `tests/Browser/Storefront/ResponsiveTest.php`

**Purpose:** Verify that the storefront and admin panel render and function correctly on mobile (375x812) and tablet (768x1024) viewports.

### Viewport Definitions


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

**File:** `tests/Browser/Storefront/InventoryTest.php`

**Purpose:** Verify that inventory policies are correctly enforced on the storefront - deny-policy blocks purchasing when out of stock, continue-policy allows backorders.

### Inventory Policy Decision Matrix

| Stock Level | Policy | Storefront Display | Add-to-Cart |
|-------------|--------|--------------------|-------------|
| > 0 | `deny` | "Add to cart" button enabled | Allowed |
| > 0 | `continue` | "Add to cart" button enabled | Allowed |
| 0 | `deny` | "Sold out" text, button disabled | Blocked |
| 0 | `continue` | "Available on backorder" text, button enabled | Allowed |

---

### Test 11.1: Blocks add-to-cart for out-of-stock deny-policy product

**Preconditions:** Product #17 (0 stock, inventory policy = deny)

**Steps:**
1. Navigate to the product page for Product #17 (use actual handle from seeder)

**Expected Results:**
- Page displays "Sold out"
- "Add to cart" button is disabled or hidden (not clickable)
- No JavaScript errors

---

### Test 11.2: Allows add-to-cart for out-of-stock continue-policy product

**Preconditions:** Product #18 (0 stock, inventory policy = continue)

**Steps:**
1. Navigate to the product page for Product #18 (use actual handle from seeder)
2. Verify "Available on backorder" is displayed
3. Click "Add to cart"
4. Navigate to `/cart`

**Expected Results:**
- "Available on backorder" is visible on the product page
- Add to cart succeeds
- Cart page shows the product
- No JavaScript errors

---

### Test 11.3: Shows correct stock status for in-stock product

**Preconditions:** Product #1 (in stock)

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Add to cart" button (enabled)
- "Sold out" is NOT displayed
- "Available on backorder" is NOT displayed
- No JavaScript errors

---

### Test 11.4: Prevents adding more than available stock for deny-policy product

**Preconditions:** A product with limited stock (deny policy)

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Attempt to increase quantity beyond the available stock level

**Expected Results:**
- System prevents exceeding available inventory (either caps quantity or shows error)
- No JavaScript errors

**Total: 4 tests**

---

## Suite 12: Tenant Isolation

**File:** `tests/Browser/Storefront/TenantIsolationTest.php`

**Purpose:** Verify that multi-store tenant isolation is enforced - data from one store never leaks into another store's storefront or admin panel.

### Isolation Requirements

```mermaid
flowchart LR
    subgraph Store1[Store 1 - Acme Fashion]
        S1Products[Fashion Products]
        S1Orders[Fashion Orders]
        S1Customers[Fashion Customers]
    end
    subgraph Store2[Store 2 - Different Store]
        S2Products[Other Products]
        S2Orders[Other Orders]
        S2Customers[Other Customers]
    end
    S1Products -.-x S2Products
    S1Orders -.-x S2Orders
    S1Customers -.-x S2Customers
```

Dashed X lines indicate these must NEVER cross. No data from Store 2 should ever appear when browsing Store 1, and vice versa.

---

### Test 12.1: Store 1 only shows Store 1 products

**Preconditions:** Seeded Store 1 products

**Steps:**
1. Navigate to `/` (resolved to Store 1 via acme-fashion.test domain)

**Expected Results:**
- Page displays "Acme Fashion"
- Page displays "Classic Cotton T-Shirt"
- Products from other stores (e.g., electronics from Store 2) are absent
- No JavaScript errors

---

### Test 12.2: Store 1 collections only contain Store 1 products

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- Only Store 1 products appear in the collection grid
- No JavaScript errors

---

### Test 12.3: Admin cannot access other store data

**Preconditions:** Logged in as admin@acme.test (Store 1 owner)

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin (email: `admin@acme.test`, password: `password`)
3. Click "Products" in the sidebar
4. Review the product list
5. Click "Orders" in the sidebar
6. Review the order list

**Expected Results:**
- All products listed belong to Store 1 only
- All orders listed belong to Store 1 only
- No cross-store data is visible
- No JavaScript errors

---

### Test 12.4: Search only returns current store products

**Preconditions:** Seeded Store 1 products

**Steps:**
1. Navigate to `/search?q=product`

**Expected Results:**
- Only Store 1 products appear in search results
- No cross-store data leakage
- No JavaScript errors

---

### Test 12.5: Customer accounts are scoped to their store

**Preconditions:** customer@acme.test seeded for Store 1

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Click "Orders"

**Expected Results:**
- Customer only sees Store 1 orders (#1001, #1002, #1004)
- No JavaScript errors

**Total: 5 tests**

---

## Suite 13: Responsive / Mobile

**File:** `tests/Browser/Storefront/ResponsiveTest.php`

**Purpose:** Verify that the storefront and admin panel render and function correctly on mobile (375x812) and tablet (768x1024) viewports.

### Viewport Definitions


rg "inventory|stock|quantity_available|Out of stock|Available on backorder|addLine|add to cart" -n app resources tests/Feature tests/Browser specs/05-BUSINESS-LOGIC.md | sed -n '1,260p'

rg 'inventory|stock|quantity_available|Out of stock|Available on backorder|addLine|add to cart' -n app resources tests/Feature tests/Browser specs/05-BUSINESS-LOGIC.md

Question: inventory|stock|quantity_available|Out of stock|Available on backorder|addLine|add to cart

tests/Browser/Storefront/BrowsingTest.php:104:test('shows out of stock messaging for deny policy product', function (): void {
tests/Browser/Storefront/BrowsingTest.php:107:        ->assertSee('Out of stock')
tests/Browser/Storefront/BrowsingTest.php:117:        ->assertSee('Available on backorder')
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:167:            SKIP (preserve existing variant - prices, SKUs, inventory intact)
specs/05-BUSINESS-LOGIC.md:203:Each `product_variant` has exactly one `inventory_item` record (created automatically when the variant is created).
specs/05-BUSINESS-LOGIC.md:207:| `quantity_on_hand` | Total physical stock count |
specs/05-BUSINESS-LOGIC.md:213:Per-variant policy stored in `inventory_items.policy`:
specs/05-BUSINESS-LOGIC.md:217:| `deny` | Block add-to-cart when available <= 0. Storefront displays "Out of stock". |
specs/05-BUSINESS-LOGIC.md:218:| `continue` | Allow add-to-cart regardless of stock level. When available <= 0, storefront displays "Available on backorder". |
specs/05-BUSINESS-LOGIC.md:232:        C -->|Restock on refund| A
specs/05-BUSINESS-LOGIC.md:243:| **Restock** | Refund processed with restock flag | +quantity | No change | None |
specs/05-BUSINESS-LOGIC.md:306:- Re-check inventory policy for the new quantity.
specs/05-BUSINESS-LOGIC.md:343:- Releases any reserved inventory associated with active checkouts for these carts.
specs/05-BUSINESS-LOGIC.md:433:    note right of completed : Order created, inventory committed
specs/05-BUSINESS-LOGIC.md:434:    note right of expired : Reserved inventory released
specs/05-BUSINESS-LOGIC.md:498:2. Reserve inventory for all cart lines (call InventoryService.reserve for each line).
specs/05-BUSINESS-LOGIC.md:521:           Release reserved inventory
specs/05-BUSINESS-LOGIC.md:531:           inventoryAction = 'commit'
specs/05-BUSINESS-LOGIC.md:536:           inventoryAction = 'keep_reserved'
specs/05-BUSINESS-LOGIC.md:557:    8. IF inventoryAction = 'commit':
specs/05-BUSINESS-LOGIC.md:560:       // Note: for bank_transfer, inventory stays reserved until admin confirms payment
specs/05-BUSINESS-LOGIC.md:569:    12. IF inventoryAction = 'commit' AND all order lines are digital:
specs/05-BUSINESS-LOGIC.md:585:1. If inventory was reserved (status was `payment_selected`): release all reserved inventory via InventoryService.release.
specs/05-BUSINESS-LOGIC.md:979:    Backend->>Backend: Reserve inventory (if not already reserved)
specs/05-BUSINESS-LOGIC.md:986:            Backend->>Backend: Commit inventory, mark cart converted
specs/05-BUSINESS-LOGIC.md:989:            Backend->>Backend: Release reserved inventory
specs/05-BUSINESS-LOGIC.md:1037:5. Backend calls `InventoryService.commit()` for each order line (converts reserved stock to committed).
specs/05-BUSINESS-LOGIC.md:1050:1. Release reserved inventory via `InventoryService.release()`.
specs/05-BUSINESS-LOGIC.md:1130:| `restock` | boolean | No | Whether to restock inventory for refunded items. |
specs/05-BUSINESS-LOGIC.md:1160:    7. IF request.restock = true:
specs/05-BUSINESS-LOGIC.md:1162:           CALL InventoryService.restock(variant, quantity)
specs/05-BUSINESS-LOGIC.md:1255:The fulfillment guard interacts with inventory differently depending on the payment method:
specs/05-BUSINESS-LOGIC.md:1259:| `credit_card` / `paypal` | Inventory committed (on_hand decremented, reserved decremented) | N/A (already committed) | No inventory change |
specs/05-BUSINESS-LOGIC.md:1260:| `bank_transfer` | Inventory reserved only (reserved incremented) | Inventory committed (on_hand decremented, reserved decremented) | No inventory change |
specs/05-BUSINESS-LOGIC.md:1262:For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.
specs/05-BUSINESS-LOGIC.md:1595:| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
specs/05-BUSINESS-LOGIC.md:1599:| `CheckoutExpired` | Release reserved inventory |
specs/05-BUSINESS-LOGIC.md:1604:| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
specs/05-BUSINESS-LOGIC.md:1608:| `OrderCancelled` | Release reserved inventory, send cancellation email |
specs/05-BUSINESS-LOGIC.md:1619:| `App\Jobs\ExpireAbandonedCheckouts` | Every 15 minutes | Expire checkouts idle for 24+ hours, release reserved inventory |
specs/05-BUSINESS-LOGIC.md:1640:| InventoryService | `App\Services` | Reserve, release, commit, restock operations |
specs/05-BUSINESS-LOGIC.md:1651:| RefundService | `App\Services` | Refund processing and inventory restock |
tests/Browser/SmokeTest.php:79:        '/admin/invent…21464 chars truncated…  $inventory = $variant->inventoryItem;
app/Http/Resources/Storefront/V1/SearchProductResource.php:42:        if ($inventory === null) {
app/Http/Resources/Storefront/V1/SearchProductResource.php:46:        return $inventory->availableQuantity() > 0
app/Http/Resources/Storefront/V1/SearchProductResource.php:47:            || $inventory->policy === InventoryPolicy::Continue;
app/Livewire/Admin/Inventory/Index.php:17:    public string $stockFilter = 'all';
app/Livewire/Admin/Inventory/Index.php:42:            ->when($this->stockFilter === 'low', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) BETWEEN 1 AND 10'))
app/Livewire/Admin/Inventory/Index.php:43:            ->when($this->stockFilter === 'out', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) <= 0'))
app/Livewire/Admin/Inventory/Index.php:50:        return view('livewire.admin.inventory.index', [
app/Livewire/Admin/Settings/Notifications.php:75:                    'low_stock_alerts_enabled' => $validated['lowStockAlertsEnabled'],
app/Livewire/Admin/Settings/Notifications.php:76:                    'low_stock_threshold' => $validated['lowStockThreshold'],
app/Livewire/Admin/Settings/Notifications.php:106:        $this->lowStockAlertsEnabled = (bool) data_get($settings, 'notifications.low_stock_alerts_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:107:        $this->lowStockThreshold = (int) data_get($settings, 'notifications.low_stock_threshold', 5);
app/Livewire/Storefront/Collections/Show.php:70:            ->with(['variants.inventoryItem'])
app/Livewire/Storefront/Collections/Show.php:75:                $query->whereHas('variants.inventoryItem', function (Builder $query): void {
app/Livewire/Admin/Products/Form.php:82:            $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:232:        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:250:        $this->product = $product->refresh()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:403:                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
app/Livewire/Admin/Products/Form.php:800:        $this->product = $this->productForAction()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Storefront/Search/Index.php:110:            'in_stock' => $this->inStock,
app/Livewire/Storefront/Home.php:26:            ->with(['variants.inventoryItem'])
app/Livewire/Storefront/Products/Show.php:52:        $max = $variant?->inventoryItem?->policy === InventoryPolicy::Deny
app/Livewire/Storefront/Products/Show.php:53:            ? max(1, $variant->inventoryItem->availableQuantity())
app/Livewire/Storefront/Products/Show.php:78:        app(CartService::class)->addLine(
app/Livewire/Storefront/Products/Show.php:109:            ->with(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections'])
app/Livewire/Storefront/Products/Show.php:141:        $inventory = $variant->inventoryItem;
app/Livewire/Storefront/Products/Show.php:143:        return $inventory?->policy === InventoryPolicy::Continue
app/Livewire/Storefront/Products/Show.php:144:            || ($inventory?->availableQuantity() ?? 0) >= $this->quantity;
app/Livewire/Storefront/Products/Show.php:150:    public function stockState(): array
app/Livewire/Storefront/Products/Show.php:153:        $inventory = $variant?->inventoryItem;
app/Livewire/Storefront/Products/Show.php:155:        if (! $inventory) {
app/Livewire/Storefront/Products/Show.php:159:        if ($inventory->policy === InventoryPolicy::Continue && $inventory->availableQuantity() <= 0) {
app/Livewire/Storefront/Products/Show.php:160:            return ['message' => 'Available on backorder', 'class' => 'text-blue-700 dark:text-blue-300'];
app/Livewire/Storefront/Products/Show.php:163:        if ($inventory->availableQuantity() <= 0) {
app/Livewire/Storefront/Products/Show.php:164:            return ['message' => 'Out of stock', 'class' => 'text-red-700 dark:text-red-300'];
app/Livewire/Storefront/Products/Show.php:167:        if ($inventory->availableQuantity() <= 10) {
app/Livewire/Storefront/Products/Show.php:168:            return ['message' => "Only {$inventory->availableQuantity()} left in stock", 'class' => 'text-amber-700 dark:text-amber-300'];
app/Livewire/Storefront/Products/Show.php:171:        return ['message' => 'In stock', 'class' => 'text-green-700 dark:text-green-300'];
app/Livewire/Storefront/Products/Show.php:181:            'stockState' => $this->stockState(),
app/Livewire/Admin/Products/Index.php:93:            ->with(['variants.inventoryItem'])


Output:
Chunk ID: 009dcb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 7866
Output:
tests/Browser/Storefront/BrowsingTest.php:104:test('shows out of stock messaging for deny policy product', function (): void {
tests/Browser/Storefront/BrowsingTest.php:107:        ->assertSee('Out of stock')
tests/Browser/Storefront/BrowsingTest.php:117:        ->assertSee('Available on backorder')
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:167:            SKIP (preserve existing variant - prices, SKUs, inventory intact)
specs/05-BUSINESS-LOGIC.md:203:Each `product_variant` has exactly one `inventory_item` record (created automatically when the variant is created).
specs/05-BUSINESS-LOGIC.md:207:| `quantity_on_hand` | Total physical stock count |
specs/05-BUSINESS-LOGIC.md:213:Per-variant policy stored in `inventory_items.policy`:
specs/05-BUSINESS-LOGIC.md:217:| `deny` | Block add-to-cart when available <= 0. Storefront displays "Out of stock". |
specs/05-BUSINESS-LOGIC.md:218:| `continue` | Allow add-to-cart regardless of stock level. When available <= 0, storefront displays "Available on backorder". |
specs/05-BUSINESS-LOGIC.md:232:        C -->|Restock on refund| A
specs/05-BUSINESS-LOGIC.md:243:| **Restock** | Refund processed with restock flag | +quantity | No change | None |
specs/05-BUSINESS-LOGIC.md:306:- Re-check inventory policy for the new quantity.
specs/05-BUSINESS-LOGIC.md:343:- Releases any reserved inventory associated with active checkouts for these carts.
specs/05-BUSINESS-LOGIC.md:433:    note right of completed : Order created, inventory committed
specs/05-BUSINESS-LOGIC.md:434:    note right of expired : Reserved inventory released
specs/05-BUSINESS-LOGIC.md:498:2. Reserve inventory for all cart lines (call InventoryService.reserve for each line).
specs/05-BUSINESS-LOGIC.md:521:           Release reserved inventory
specs/05-BUSINESS-LOGIC.md:531:           inventoryAction = 'commit'
specs/05-BUSINESS-LOGIC.md:536:           inventoryAction = 'keep_reserved'
specs/05-BUSINESS-LOGIC.md:557:    8. IF inventoryAction = 'commit':
specs/05-BUSINESS-LOGIC.md:560:       // Note: for bank_transfer, inventory stays reserved until admin confirms payment
specs/05-BUSINESS-LOGIC.md:569:    12. IF inventoryAction = 'commit' AND all order lines are digital:
specs/05-BUSINESS-LOGIC.md:585:1. If inventory was reserved (status was `payment_selected`): release all reserved inventory via InventoryService.release.
specs/05-BUSINESS-LOGIC.md:979:    Backend->>Backend: Reserve inventory (if not already reserved)
specs/05-BUSINESS-LOGIC.md:986:            Backend->>Backend: Commit inventory, mark cart converted
specs/05-BUSINESS-LOGIC.md:989:            Backend->>Backend: Release reserved inventory
specs/05-BUSINESS-LOGIC.md:1037:5. Backend calls `InventoryService.commit()` for each order line (converts reserved stock to committed).
specs/05-BUSINESS-LOGIC.md:1050:1. Release reserved inventory via `InventoryService.release()`.
specs/05-BUSINESS-LOGIC.md:1130:| `restock` | boolean | No | Whether to restock inventory for refunded items. |
specs/05-BUSINESS-LOGIC.md:1160:    7. IF request.restock = true:
specs/05-BUSINESS-LOGIC.md:1162:           CALL InventoryService.restock(variant, quantity)
specs/05-BUSINESS-LOGIC.md:1255:The fulfillment guard interacts with inventory differently depending on the payment method:
specs/05-BUSINESS-LOGIC.md:1259:| `credit_card` / `paypal` | Inventory committed (on_hand decremented, reserved decremented) | N/A (already committed) | No inventory change |
specs/05-BUSINESS-LOGIC.md:1260:| `bank_transfer` | Inventory reserved only (reserved incremented) | Inventory committed (on_hand decremented, reserved decremented) | No inventory change |
specs/05-BUSINESS-LOGIC.md:1262:For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.
specs/05-BUSINESS-LOGIC.md:1595:| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
specs/05-BUSINESS-LOGIC.md:1599:| `CheckoutExpired` | Release reserved inventory |
specs/05-BUSINESS-LOGIC.md:1604:| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
specs/05-BUSINESS-LOGIC.md:1608:| `OrderCancelled` | Release reserved inventory, send cancellation email |
specs/05-BUSINESS-LOGIC.md:1619:| `App\Jobs\ExpireAbandonedCheckouts` | Every 15 minutes | Expire checkouts idle for 24+ hours, release reserved inventory |
specs/05-BUSINESS-LOGIC.md:1640:| InventoryService | `App\Services` | Reserve, release, commit, restock operations |
specs/05-BUSINESS-LOGIC.md:1651:| RefundService | `App\Services` | Refund processing and inventory restock |
tests/Browser/SmokeTest.php:79:        '/admin/inventory' => 'Inventory',
tests/Feature/Orders/OrderServiceTest.php:57:function orderCompletionVariant(Store $store, int $price = 2500, int $stock = 10): ProductVariant
tests/Feature/Orders/OrderServiceTest.php:69:            'quantity_on_hand' => $stock,
tests/Feature/Orders/OrderServiceTest.php:119:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Orders/OrderServiceTest.php:145:test('checkout completion creates a paid order and commits inventory', function () {
tests/Feature/Orders/OrderServiceTest.php:179:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Orders/OrderServiceTest.php:181:    expect($inventory->quantity_on_hand)->toBe(8)
tests/Feature/Orders/OrderServiceTest.php:182:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/OrderServiceTest.php:268:test('bank transfer completion creates a pending order and keeps inventory reserved', function () {
tests/Feature/Orders/OrderServiceTest.php:281:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Orders/OrderServiceTest.php:283:    expect($inventory->quantity_on_hand)->toBe(10)
tests/Feature/Orders/OrderServiceTest.php:284:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Orders/OrderServiceTest.php:290:test('payment failures release reserved inventory and allow checkout retry', function () {
tests/Feature/Orders/OrderServiceTest.php:298:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Orders/OrderServiceTest.php:301:        ->and($inventory->quantity_on_hand)->toBe(10)
tests/Feature/Orders/OrderServiceTest.php:302:        ->and($inventory->quantity_reserved)->toBe(0)
tests/Feature/Orders/BankTransferOrderTest.php:80:test('confirming bank transfer payment captures payment and commits reserved inventory', function () {
tests/Feature/Orders/BankTransferOrderTest.php:86:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Orders/BankTransferOrderTest.php:91:        ->and($inventory->quantity_on_hand)->toBe(8)
tests/Feature/Orders/BankTransferOrderTest.php:92:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Orders/RefundServiceTest.php:70:test('refund service processes a partial line refund and restocks inventory', function () {
tests/Feature/Orders/RefundServiceTest.php:78:        'restock' => true,
tests/Feature/Storefront/CartCheckoutUiTest.php:50:test('product detail add to cart persists a session cart', function () {
tests/Feature/Storefront/CartCheckoutUiTest.php:70:    $line = app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Storefront/CartCheckoutUiTest.php:91:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Storefront/CartCheckoutUiTest.php:129:    app(CartService::class)->addLine($customerCart, $variant->getKey(), 1);
tests/Feature/Storefront/CartCheckoutUiTest.php:130:    app(CartService::class)->addLine($guestCart, $variant->getKey(), 2);
tests/Feature/Storefront/CartCheckoutUiTest.php:147:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Storefront/CartCheckoutUiTest.php:175:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Storefront/CartCheckoutUiTest.php:180:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Admin/OrderManagementTest.php:170:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Admin/OrderManagementTest.php:175:        ->and($inventory->quantity_on_hand)->toBe(8)
tests/Feature/Admin/OrderManagementTest.php:176:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Catalog/ProductServiceTest.php:32:test('product service creates products with a default variant and inventory', function () {
tests/Feature/Catalog/ProductServiceTest.php:45:        ->and($product->variants->first()->inventoryItem)->not->toBeNull();
tests/Feature/Storefront/OrderViewsTest.php:64:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Storefront/OrderViewsTest.php:98:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Storefront/OrderViewsTest.php:119:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Storefront/OrderViewsTest.php:123:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Catalog/CatalogFoundationTest.php:23:        ->and(Schema::hasColumns('inventory_items', ['store_id', 'variant_id', 'quantity_on_hand', 'quantity_reserved', 'policy']))->toBeTrue()
tests/Feature/Catalog/CatalogFoundationTest.php:33:        ->and(Product::withoutGlobalScopes()->where('handle', 'limited-edition-sneakers')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('deny')
tests/Feature/Catalog/CatalogFoundationTest.php:34:        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-denim-jacket')->first()?->variants()->withoutGlobalScopes()->first()?->inventoryItem?->policy?->value)->toBe('continue');
tests/Feature/Catalog/CatalogFoundationTest.php:72:test('variants create inventory for their product store regardless of resolved store scope', function () {
tests/Feature/Catalog/CatalogFoundationTest.php:80:    $inventory = InventoryItem::withoutGlobalScopes()
tests/Feature/Catalog/CatalogFoundationTest.php:84:    expect($inventory)->not->toBeNull()
tests/Feature/Catalog/CatalogFoundationTest.php:85:        ->and($inventory->store_id)->toBe($productStore->getKey())
tests/Feature/Catalog/CatalogFoundationTest.php:86:        ->and($inventory->policy)->toBe(InventoryPolicy::Deny);
tests/Feature/Catalog/CatalogFoundationTest.php:89:test('inventory store must match the variant product store', function () {
tests/Feature/Checkout/PricingServicesTest.php:73:    app(CartService::class)->addLine($cart, $variant->getKey(), $quantity);
tests/Feature/Checkout/PricingServicesTest.php:235:    app(CartService::class)->addLine($usedCart, $variant->getKey(), 1);
tests/Feature/Checkout/PricingServicesTest.php:246:    app(CartService::class)->addLine($otherCart, $variant->getKey(), 1);
tests/Feature/Checkout/PricingServicesTest.php:256:    app(CartService::class)->addLine($cart, $physicalVariant->getKey(), 2);
tests/Feature/Checkout/PricingServicesTest.php:257:    app(CartService::class)->addLine($cart, $digitalVariant->getKey(), 1);
tests/Feature/Catalog/InventoryServiceTest.php:12:test('inventory reserve release commit and restock update quantities transactionally', function () {
tests/Feature/Catalog/InventoryServiceTest.php:18:    $item = $variant->inventoryItem()->firstOrFail();
tests/Feature/Catalog/InventoryServiceTest.php:33:    $service->restock($item, 5);
tests/Feature/Catalog/InventoryServiceTest.php:37:test('deny policy blocks reservations above available stock while continue allows it', function () {
tests/Feature/Catalog/InventoryServiceTest.php:43:    $denyItem = $denyVariant->inventoryItem()->firstOrFail();
tests/Feature/Catalog/InventoryServiceTest.php:51:    $continueItem = $continueVariant->inventoryItem()->firstOrFail();
tests/Feature/Api/StorefrontCheckoutApiTest.php:138:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Api/StorefrontCheckoutApiTest.php:141:        ->and($inventory->quantity_reserved)->toBe(2);
tests/Feature/Catalog/CatalogUiTest.php:73:        ->get('/admin/inventory')
tests/Feature/Catalog/CatalogUiTest.php:100:        ->and($product->variants()->first()?->inventoryItem?->quantity_on_hand)->toBe(50);
tests/Feature/Admin/SettingsManagementTest.php:199:        ->and(data_get($settings, 'notifications.low_stock_alerts_enabled'))->toBeTrue()
tests/Feature/Admin/SettingsManagementTest.php:200:        ->and(data_get($settings, 'notifications.low_stock_threshold'))->toBe(3);
tests/Feature/Checkout/CheckoutServiceTest.php:92:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Checkout/CheckoutServiceTest.php:126:    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
tests/Feature/Checkout/CheckoutServiceTest.php:138:    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
tests/Feature/Checkout/CheckoutServiceTest.php:165:    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
tests/Feature/Checkout/CheckoutServiceTest.php:195:    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
tests/Feature/Api/StorefrontSearchApiTest.php:52:        'in_stock' => true,
tests/Feature/Api/StorefrontOrderApiTest.php:142:    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();
tests/Feature/Api/StorefrontOrderApiTest.php:145:        ->and($inventory->quantity_reserved)->toBe(0);
tests/Feature/Catalog/VariantMatrixServiceTest.php:13:test('variant matrix rebuild creates missing cartesian combinations and inventory', function () {
tests/Feature/Catalog/VariantMatrixServiceTest.php:38:        ->and($product->variants()->whereHas('inventoryItem')->count())->toBe(4);
tests/Feature/Catalog/VariantMatrixServiceTest.php:65:        ->and($product->variants()->first()->inventoryItem)->not->toBeNull();
tests/Feature/Api/AdminStoreSettingsApiTest.php:109:                    'low_stock_threshold' => 12,
tests/Feature/Api/AdminStoreSettingsApiTest.php:114:        ->assertJsonPath('data.settings_json.notifications.low_stock_threshold', 12);
tests/Feature/Api/AdminCatalogApiTest.php:69:        ->assertJsonPath('data.0.total_inventory', 50)
tests/Feature/Api/AdminCatalogApiTest.php:77:        ->assertJsonPath('data.variants.0.inventory.quantity_on_hand', 50);
tests/Feature/Cart/CartServiceTest.php:27:function cartServiceVariant(Store $store, int $price = 2500, int $stock = 20, InventoryPolicy $policy = InventoryPolicy::Deny): ProductVariant
tests/Feature/Cart/CartServiceTest.php:39:            'quantity_on_hand' => $stock,
tests/Feature/Cart/CartServiceTest.php:49:    $variant = cartServiceVariant($store, price: 2500, stock: 20);
tests/Feature/Cart/CartServiceTest.php:57:    $line = $service->addLine($cart, $variant->getKey(), 2, expectedVersion: 1);
tests/Feature/Cart/CartServiceTest.php:64:    $service->addLine($cart, $variant->getKey(), 1, expectedVersion: 2);
tests/Feature/Cart/CartServiceTest.php:83:test('cart service rejects stale versions inactive products and insufficient stock', function () {
tests/Feature/Cart/CartServiceTest.php:85:    $variant = cartServiceVariant($store, stock: 2);
tests/Feature/Cart/CartServiceTest.php:89:    expect(fn () => $service->addLine($cart, $variant->getKey(), 1, expectedVersion: 99))
tests/Feature/Cart/CartServiceTest.php:100:    expect(fn () => $service->addLine($cart, $draftVariant->getKey(), 1))
tests/Feature/Cart/CartServiceTest.php:103:    expect(fn () => $service->addLine($cart, $variant->getKey(), 5))
tests/Feature/Cart/CartServiceTest.php:109:    $firstVariant = cartServiceVariant($store, stock: 0, policy: InventoryPolicy::Continue);
tests/Feature/Cart/CartServiceTest.php:110:    $secondVariant = cartServiceVariant($store, stock: 10);
tests/Feature/Cart/CartServiceTest.php:116:    $service->addLine($guestCart, $firstVariant->getKey(), 5);
tests/Feature/Cart/CartServiceTest.php:117:    $service->addLine($customerCart, $firstVariant->getKey(), 2);
tests/Feature/Cart/CartServiceTest.php:118:    $service->addLine($customerCart, $secondVariant->getKey(), 3);
tests/Feature/Api/AdminShippingSettingsApiTest.php:52:            'regions_json' => ['stockholm'],
resources/views/layouts/app/sidebar.blade.php:43:                    <flux:sidebar.item icon="archive-box" :href="route('admin.inventory.index')" :current="request()->routeIs('admin.inventory.*')" wire:navigate>
resources/views/components/storefront/product-card.blade.php:6:        && $product->variants->every(fn ($variant) => $variant->inventoryItem?->policy?->value === 'deny' && $variant->inventoryItem?->availableQuantity() <= 0);
resources/views/livewire/admin/inventory/index.blade.php:4:        <flux:text class="mt-1">Review stock levels by product variant.</flux:text>
resources/views/livewire/admin/inventory/index.blade.php:8:        <flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="Search by product or SKU..." aria-label="Search inventory" />
resources/views/livewire/admin/inventory/index.blade.php:10:        <flux:select wire:model.live="stockFilter" aria-label="Stock filter">
resources/views/livewire/admin/inventory/index.blade.php:11:            <flux:select.option value="all">All stock</flux:select.option>
resources/views/livewire/admin/inventory/index.blade.php:12:            <flux:select.option value="low">Low stock</flux:select.option>
resources/views/livewire/admin/inventory/index.blade.php:13:            <flux:select.option value="out">Out of stock</flux:select.option>
resources/views/livewire/admin/inventory/index.blade.php:32:
getKey() }}">
resources/views/livewire/admin/inventory/index.blade.php:48:                            No inventory items found.
app/Services/SearchService.php:319:        if (filter_var($filters['in_stock'] ?? false, FILTER_VALIDATE_BOOL)) {
app/Services/SearchService.php:324:                    ->join('inventory_items', 'inventory_items.variant_id', '=', 'product_variants.id')
app/Services/SearchService.php:328:                            ->whereRaw('inventory_items.quantity_on_hand > inventory_items.quantity_reserved')
app/Services/SearchService.php:329:                            ->orWhere('inventory_items.policy', InventoryPolicy::Continue->value);
app/Services/SearchService.php:368:            ->with(['store', 'variants.inventoryItem', 'media'])
resources/views/livewire/admin/products/index.blade.php:5:            <flux:text class="mt-1">Manage catalog items, variants, status, and inventory.</flux:text>
resources/views/livewire/admin/products/index.blade.php:64:                            $inventory = $product->variants->sum(fn ($variant) => $variant->inventoryItem?->quantity_on_hand ?? 0);
resources/views/livewire/admin/products/index.blade.php:93:                            {{ $inventory }}
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php:25:            'restock' => (bool) $request->validated('restock', false),
app/Services/ThemeSettingsService.php:69:                    'subheading' => 'Browse a scoped demo catalog with variants, inventory states, sale pricing, and digital products.',
resources/views/livewire/admin/settings/notifications.blade.php:61:                    <flux:switch wire:model="lowStockAlertsEnabled" label="Low stock alerts" align="left" />
resources/views/livewire/admin/settings/notifications.blade.php:62:                    <flux:input wire:model="lowStockThreshold" type="number" min="0" max="999" label="Low stock threshold" />
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:56:            'settings_json.notifications.low_stock_alerts_enabled' => ['sometimes', 'boolean'],
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:57:            'settings_json.notifications.low_stock_threshold' => ['sometimes', 'integer', 'min:0', 'max:1000'],
app/Http/Controllers/Api/Storefront/V1/CartLineController.php:23:            $carts->addLine(
app/Http/Controllers/Api/Admin/V1/ProductController.php:31:                'variants.inventoryItem',
app/Http/Controllers/Api/Admin/V1/ProductController.php:83:            'variants.inventoryItem',
app/Services/CartService.php:21:        private readonly InventoryService $inventory,
app/Services/CartService.php:120:    public function addLine(Cart $cart, int $variantId, int $quantity, ?int $expectedVersion = null): CartLine
app/Services/CartService.php:136:            $this->assertAvailable($variant->inventoryItem, $newQuantity);
app/Services/CartService.php:172:            $this->assertAvailable($variant->inventoryItem, $quantity);
app/Services/CartService.php:244:                'inventoryItem' => fn ($query) => $query->withoutGlobalScopes(),
app/Services/CartService.php:260:        if (! $variant->inventoryItem instanceof InventoryItem) {
app/Services/CartService.php:261:            throw InvalidCartOperationException::because('Variant inventory is missing.');
app/Services/CartService.php:269:        if (! $this->inventory->checkAvailability($item, $quantity)) {
app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php:22:            'restock' => ['sometimes', 'boolean'],
app/Services/CheckoutService.php:22:        private readonly InventoryService $inventory,
app/Services/CheckoutService.php:149:                    $inventory = InventoryItem::withoutGlobalScopes()
app/Services/CheckoutService.php:153:                    $this->inventory->reserve($inventory, $line->quantity);
app/Services/CheckoutService.php:180:                        $inventory = InventoryItem::withoutGlobalScopes()
app/Services/CheckoutService.php:184:                        $this->inventory->release($inventory, $line->quantity);
app/Services/OrderService.php:36:        private readonly InventoryService $inventory,
app/Services/OrderService.php:358:            $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
app/Services/OrderService.php:366:                $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
app/Services/OrderService.php:375:                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
app/Services/OrderService.php:390:                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
app/Services/OrderService.php:422:    private function inventoryItemForVariant(int $variantId): InventoryItem
resources/views/livewire/storefront/collections/show.blade.php:22:                <flux:checkbox wire:model.live="inStock" label="In stock" />
resources/views/livewire/storefront/search/index.blade.php:24:                <flux:checkbox wire:model.live="inStock" label="In stock" />
app/Services/ProductService.php:184:                'policy' => $variantData['inventory_policy'] ?? 'deny',
resources/views/livewire/storefront/products/show.blade.php:61:
{{ $stockState['message'] }}

app/Services/RefundService.php:23:        private readonly InventoryService $inventory,
app/Services/RefundService.php:27:     * @param  array{amount?: int, lines?: array<int|string, int>, reason?: string|null, restock?: bool}  $request
app/Services/RefundService.php:52:            if ((bool) data_get($request, 'restock', false)) {
app/Services/RefundService.php:53:                $this->restock($order, $lineQuantities);
app/Services/RefundService.php:161:    private function restock(Order $order, Collection $lineQuantities): void
app/Services/RefundService.php:163:        $linesToRestock = $lineQuantities->isNotEmpty()
app/Services/RefundService.php:167:        $linesToRestock->each(function (int $quantity, int $lineId) use ($order): void {
app/Services/RefundService.php:179:                $this->inventory->restock($item, $quantity);
app/Services/InventoryService.php:49:                throw new InvalidArgumentException('Cannot release more inventory than is reserved.');
app/Services/InventoryService.php:66:                throw new InvalidArgumentException('Cannot commit more inventory than is reserved.');
app/Services/InventoryService.php:76:    public function restock(InventoryItem $item, int $quantity): void
app/Http/Resources/Admin/V1/ProductResource.php:31:            'total_inventory' => $this->when($this->relationLoaded('variants'), fn (): int => $this->variants->sum(
app/Http/Resources/Admin/V1/ProductResource.php:32:                fn (ProductVariant $variant): int => $variant->inventoryItem?->quantity_on_hand ?? 0,
app/Http/Resources/Admin/V1/ProductResource.php:67:                    'inventory' => [
app/Http/Resources/Admin/V1/ProductResource.php:68:                        'quantity_on_hand' => $variant->inventoryItem?->quantity_on_hand ?? 0,
app/Http/Resources/Admin/V1/ProductResource.php:69:                        'quantity_reserved' => $variant->inventoryItem?->quantity_reserved ?? 0,
app/Http/Resources/Admin/V1/ProductResource.php:70:                        'policy' => $variant->inventoryItem?->policy?->value,
app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php:26:            'filters.in_stock' => ['nullable', 'boolean'],
app/Models/ProductVariant.php:74:            if ($variant->inventoryItem()->exists()) {
app/Models/ProductVariant.php:140:    public function inventoryItem(): HasOne
app/Http/Resources/Storefront/V1/SearchProductResource.php:33:            'in_stock' => $variant instanceof ProductVariant ? $this->variantIsInStock($variant) : false,
app/Http/Resources/Storefront/V1/SearchProductResource.php:40:        $inventory = $variant->inventoryItem;
app/Http/Resources/Storefront/V1/SearchProductResource.php:42:        if ($inventory === null) {
app/Http/Resources/Storefront/V1/SearchProductResource.php:46:        return $inventory->availableQuantity() > 0
app/Http/Resources/Storefront/V1/SearchProductResource.php:47:            || $inventory->policy === InventoryPolicy::Continue;
app/Livewire/Admin/Inventory/Index.php:17:    public string $stockFilter = 'all';
app/Livewire/Admin/Inventory/Index.php:42:            ->when($this->stockFilter === 'low', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) BETWEEN 1 AND 10'))
app/Livewire/Admin/Inventory/Index.php:43:            ->when($this->stockFilter === 'out', fn (Builder $query) => $query->whereRaw('(quantity_on_hand - quantity_reserved) <= 0'))
app/Livewire/Admin/Inventory/Index.php:50:        return view('livewire.admin.inventory.index', [
app/Livewire/Admin/Settings/Notifications.php:75:                    'low_stock_alerts_enabled' => $validated['lowStockAlertsEnabled'],
app/Livewire/Admin/Settings/Notifications.php:76:                    'low_stock_threshold' => $validated['lowStockThreshold'],
app/Livewire/Admin/Settings/Notifications.php:106:        $this->lowStockAlertsEnabled = (bool) data_get($settings, 'notifications.low_stock_alerts_enabled', true);
app/Livewire/Admin/Settings/Notifications.php:107:        $this->lowStockThreshold = (int) data_get($settings, 'notifications.low_stock_threshold', 5);
app/Livewire/Storefront/Collections/Show.php:70:            ->with(['variants.inventoryItem'])
app/Livewire/Storefront/Collections/Show.php:75:                $query->whereHas('variants.inventoryItem', function (Builder $query): void {
app/Livewire/Admin/Products/Form.php:82:            $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:232:        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:250:        $this->product = $product->refresh()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Admin/Products/Form.php:403:                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
app/Livewire/Admin/Products/Form.php:800:        $this->product = $this->productForAction()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
app/Livewire/Storefront/Search/Index.php:110:            'in_stock' => $this->inStock,
app/Livewire/Storefront/Home.php:26:            ->with(['variants.inventoryItem'])
app/Livewire/Storefront/Products/Show.php:52:        $max = $variant?->inventoryItem?->policy === InventoryPolicy::Deny
app/Livewire/Storefront/Products/Show.php:53:            ? max(1, $variant->inventoryItem->availableQuantity())
app/Livewire/Storefront/Products/Show.php:78:        app(CartService::class)->addLine(
app/Livewire/Storefront/Products/Show.php:109:            ->with(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections'])
app/Livewire/Storefront/Products/Show.php:141:        $inventory = $variant->inventoryItem;
app/Livewire/Storefront/Products/Show.php:143:        return $inventory?->policy === InventoryPolicy::Continue
app/Livewire/Storefront/Products/Show.php:144:            || ($inventory?->availableQuantity() ?? 0) >= $this->quantity;
app/Livewire/Storefront/Products/Show.php:150:    public function stockState(): array
app/Livewire/Storefront/Products/Show.php:153:        $inventory = $variant?->inventoryItem;
app/Livewire/Storefront/Products/Show.php:155:        if (! $inventory) {
app/Livewire/Storefront/Products/Show.php:159:        if ($inventory->policy === InventoryPolicy::Continue && $inventory->availableQuantity() <= 0) {
app/Livewire/Storefront/Products/Show.php:160:            return ['message' => 'Available on backorder', 'class' => 'text-blue-700 dark:text-blue-300'];
app/Livewire/Storefront/Products/Show.php:163:        if ($inventory->availableQuantity() <= 0) {
app/Livewire/Storefront/Products/Show.php:164:            return ['message' => 'Out of stock', 'class' => 'text-red-700 dark:text-red-300'];
app/Livewire/Storefront/Products/Show.php:167:        if ($inventory->availableQuantity() <= 10) {
app/Livewire/Storefront/Products/Show.php:168:            return ['message' => "Only {$inventory->availableQuantity()} left in stock", 'class' => 'text-amber-700 dark:text-amber-300'];
app/Livewire/Storefront/Products/Show.php:171:        return ['message' => 'In stock', 'class' => 'text-green-700 dark:text-green-300'];
app/Livewire/Storefront/Products/Show.php:181:            'stockState' => $this->stockState(),
app/Livewire/Admin/Products/Index.php:93:            ->with(['variants.inventoryItem'])


Suite 11 mostly matches existing behavior: Suite 7 already checks the out-of-stock and backorder messaging, but Suite 11 adds a stock-limit quantity interaction. I’m reading the product/cart components to see whether the browser suite can cover the existing controls or whether the UI needs a customer-facing stock error.


sed -n '1,260p' app/Livewire/Storefront/Products/Show.php


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


sed -n '1,320p' app/Livewire/Storefront/Cart/Show.php


sed -n '1,360p' resources/views/livewire/storefront/cart/show.blade.php


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

    <x-storefront.breadcrumbs :items="[
        ['label' => $product->collections->first()?->title ?? 'Products', 'url' => $product->collections->first() ? route('collections.show', $product->collections->first()->handle) : route('collections.index')],
        ['label' => $product->title],
    ]" />

                <flux:icon name="shopping-bag" class="size-20" />

                    @if ($selectedVariant?->compare_at_amount && $selectedVariant->compare_at_amount > $selectedVariant->price_amount)
                        Sale
                    @endif
                    @foreach ($product->tags ?? [] as $tag)
                        {{ $tag }}
                    @endforeach

{{ $product->title }}

                @if ($selectedVariant)
                    <x-storefront.price :amount="$selectedVariant->price_amount" :compare-at="$selectedVariant->compare_at_amount" :currency="$selectedVariant->currency" class="text-2xl" aria-live="polite" />
                @endif

            @if ($product->options->isNotEmpty())

                    @foreach ($product->options as $option)
                        <fieldset class="space-y-3" wire:key="product-option-selector-{{ $option->getKey() }}">
                            <legend class="text-sm font-medium text-zinc-950 dark:text-white">{{ $option->name }}</legend>

                                @foreach ($option->values as $value)
                                    @php($selected = ($selectedOptions[$option->name] ?? null) === $value->value)
                                    <button
                                        type="button"
                                        wire:click="selectOption(@js($option->name), @js($value->value))"
                                        @class([
                                            'min-h-10 rounded-md border px-4 py-2 text-sm font-medium transition',
                                            'border-blue-700 bg-blue-50 text-blue-800 dark:border-blue-300 dark:bg-blue-950 dark:text-blue-100' => $selected,
                                            'border-zinc-200 text-zinc-700 hover:border-zinc-400 dark:border-zinc-700 dark:text-zinc-200 dark:hover:border-zinc-500' => ! $selected,
                                        ])
                                        aria-pressed="{{ $selected ? 'true' : 'false' }}"
                                    >
                                        {{ $value->value }}
                                    </button>
                                @endforeach

                        </fieldset>
                    @endforeach

            @endif

{{ $stockState['message'] }}

                    <button type="button" wire:click="decreaseQuantity" class="flex size-11 items-center justify-center border-r border-zinc-200 text-lg dark:border-zinc-700" aria-label="Decrease quantity">-</button>
                    <input wire:model.live="quantity" type="number" min="1" class="h-11 w-full border-0 bg-transparent text-center text-sm" aria-label="Quantity">
                    <button type="button" wire:click="increaseQuantity" class="flex size-11 items-center justify-center border-l border-zinc-200 text-lg dark:border-zinc-700" aria-label="Increase quantity">+</button>

                <flux:button wire:click="addToCart" variant="primary" class="w-full" :disabled="! $this->canAddToCart()">
                    {{ $this->canAddToCart() ? 'Add to cart' : 'Sold out' }}
                </flux:button>

            @if ($product->description_html)

                    {!! $product->description_html !!}

            @endif

Vendor

{{ $product->vendor ?: '-' }}

Product type

{{ $product->product_type ?: '-' }}

                @if ($selectedVariant)

SKU

{{ $selectedVariant->sku ?: '-' }}

Shipping

{{ $selectedVariant->requires_shipping ? 'Required' : 'Digital delivery' }}

                @endif


sed -n '1,340p' app/Services/CartService.php


sed -n '1,260p' app/Livewire/Storefront/Products/Show.php

<?php

namespace App\Livewire\Storefront\Products;

use App\Enums\InventoryPolicy;
use App\Models\Customer;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class Show extends Component
{
    public string $handle;

    public int $storeId;

    /**
     * @var array<string, string>
     */
    public array $selectedOptions = [];

    public int $quantity = 1;

    public function mount(string $handle): void
    {
        $this->handle = $handle;
        $this->storeId = $this->store()->getKey();

        $variant = $this->product()->variants->firstWhere('is_default', true)
            ?? $this->product()->variants->first();

        if ($variant instanceof ProductVariant) {
            $this->selectedOptions = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();
        }
    }

    public function selectOption(string $optionName, string $value): void
    {
        $this->selectedOptions[$optionName] = $value;
        $this->quantity = 1;
    }

    public function increaseQuantity(): void
    {
        $variant = $this->selectedVariant();
        $max = $variant?->inventoryItem?->policy === InventoryPolicy::Deny
            ? max(1, $variant->inventoryItem->availableQuantity())
            : null;

        if ($max === null || $this->quantity < $max) {
            $this->quantity++;
        }
    }

    public function decreaseQuantity(): void
    {
        $this->quantity = max(1, $this->quantity - 1);
    }

    public function addToCart(): void
    {
        $store = $this->store();
        $variant = $this->selectedVariant();

        if (! $variant instanceof ProductVariant || ! $this->canAddToCart()) {
            return;
        }

        $customer = Auth::guard('customer')->user();
        $customer = $customer instanceof Customer ? $customer : null;

        app(CartService::class)->addLine(
            app(CartService::class)->getOrCreateForSession($store, $customer),
            $variant->getKey(),
            $this->quantity,
        );

        $this->dispatch('cart-updated');
        $this->dispatch('toast', type: 'success', message: __('Added to cart'));
    }

    public function store(): Store
    {
        if (isset($this->storeId)) {
            $store = Store::query()->findOrFail($this->storeId);
            app()->instance('current_store', $store);

            return $store;
        }

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    public function product(): Product
    {
        $this->store();

        return Product::query()
            ->with(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections'])
            ->where('handle', $this->handle)
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->firstOrFail();
    }

    public function selectedVariant(): ?ProductVariant
    {
        $product = $this->product();

        if ($product->options->isEmpty()) {
            return $product->variants->first();
        }

        return $product->variants->first(function (ProductVariant $variant): bool {
            $options = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();

            return $options === $this->selectedOptions;
        });
    }

    public function canAddToCart(): bool
    {
        $variant = $this->selectedVariant();

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

        $inventory = $variant->inventoryItem;

        return $inventory?->policy === InventoryPolicy::Continue
            || ($inventory?->availableQuantity() ?? 0) >= $this->quantity;
    }

    /**
     * @return array{message: string, class: string}
     */
    public function stockState(): array
    {
        $variant = $this->selectedVariant();
        $inventory = $variant?->inventoryItem;

        if (! $inventory) {
            return ['message' => 'Unavailable', 'class' => 'text-red-700 dark:text-red-300'];
        }

        if ($inventory->policy === InventoryPolicy::Continue && $inventory->availableQuantity() <= 0) {
            return ['message' => 'Available on backorder', 'class' => 'text-blue-700 dark:text-blue-300'];
        }

        if ($inventory->availableQuantity() <= 0) {
            return ['message' => 'Out of stock', 'class' => 'text-red-700 dark:text-red-300'];
        }

        if ($inventory->availableQuantity() <= 10) {
            return ['message' => "Only {$inventory->availableQuantity()} left in stock", 'class' => 'text-amber-700 dark:text-amber-300'];
        }

        return ['message' => 'In stock', 'class' => 'text-green-700 dark:text-green-300'];
    }

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

        return view('livewire.storefront.products.show', [
            'product' => $product,
            'selectedVariant' => $this->selectedVariant(),
            'stockState' => $this->stockState(),
        ])->layout('layouts.storefront', [
            'title' => $product->title,
        ]);
    }
}