tool: search-docs

text:
# Search Results

## Database Testing Model Factories
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

## Model Factories

When testing, you may need to insert a few records into your database before executing your test. Instead of manually specifying the value of each column when you create this test data, Laravel allows you to define a set of default attributes for each of your [Eloquent models](/docs/{{version}}/eloquent) using [model factories](/docs/{{version}}/eloquent-factories).

To learn more about creating and utilizing model factories to create models, please consult the complete [model factory documentation](/docs/{{version}}/eloquent-factories). Once you have defined a model factory, you may utilize the factory within your test to create models:

```php tab=Pest
use App\Models\User;

test('models can be instantiated', function () {
    $user = User::factory()->create();

    // ...
});
```

```php tab=PHPUnit
use App\Models\User;

public function test_models_can_be_instantiated(): void
{
    $user = User::factory()->create();

    // ...
}
```

---

## Urls URLs for Named Routes Eloquent Models
Urls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md

#### Eloquent Models

You will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:

```php
echo route('post.show', ['post' => $post]);
```

---

## Eloquent Factories Factory Relationships
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

## Factory Relationships

---

## Database Testing Introduction
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

## Introduction

Laravel provides a variety of helpful tools and assertions to make it easier to test your database driven applications. In addition, Laravel model factories and seeders make it painless to create test database records using your application's Eloquent models and relationships. We'll discuss all of these powerful features in the following documentation.

---

## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Models and UUIDv7

**Likelihood Of Impact: Medium**

The `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:

```php
use Illuminate\Database\Eloquent\Concerns\HasUuids; // [tl! remove]
use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; // [tl! add]
```

The `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.

---

## Eloquent Factories Factory Relationships Defining Relationships Within Factories
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

### Defining Relationships Within Factories

To define a relationship within your model factory, you will typically assign a new factory instance to the foreign key of the relationship. This is normally done for the "inverse" relationships such as `belongsTo` and `morphTo` relationships. For example, if you would like to create a new user when creating a post, you may do the following:

```php
use App\Models\User;

/**
 * Define the model's default state.
 *
 * @return array<string, mixed>
 */
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title' => fake()->title(),
        'content' => fake()->paragraph(),
    ];
}
```

If the relationship's columns depend on the factory that defines it you may assign a closure to an attribute. The closure will receive the factory's evaluated attribute array:

```php
/**
 * Define the model's default state.
 *
 * @return array<string, mixed>
 */
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'user_type' => function (array $attributes) {
            return User::find($attributes['user_id'])->type;
        },
        'title' => fake()->title(),
        'content' => fake()->paragraph(),
    ];
}
```

---

## Scout Searching Where Clauses Customizing the Eloquent Results Query
Scout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md

#### Customizing the Eloquent Results Query

After Scout retrieves a list of matching Eloquent models from your application's search engine, Eloquent is used to retrieve all of the matching models by their primary keys. You may customize this query by invoking the `query` method. The `query` method accepts a closure that will receive the Eloquent query builder instance as an argument:

```php
use App\Models\Order;
use Illuminate\Database\Eloquent\Builder;

$orders = Order::search('Star Trek')
    ->query(fn (Builder $query) => $query->with('invoices'))
    ->get();
```

When using a third-party engine, this callback is invoked after the relevant models have already been retrieved from the search engine, so it should not be used for "filtering" results — use [Scout where clauses](#where-clauses) instead. However, when using the database engine, the `query` method's constraints are applied directly to the database query, so you may use it for filtering as well.

---

## Eloquent Factories Factory Relationships Belongs To Relationships
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

### Belongs To Relationships

Now that we have explored how to build "has many" relationships using factories, let's explore the inverse of the relationship. The `for` method may be used to define the parent model that factory created models belong to. For example, we can create three `App\Models\Post` model instances that belong to a single user:

```php
use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
    ->count(3)
    ->for(User::factory()->state([
        'name' => 'Jessica Archer',
    ]))
    ->create();
```

If you already have a parent model instance that should be associated with the models you are creating, you may pass the model instance to the `for` method:

```php
$user = User::factory()->create();

$posts = Post::factory()
    ->count(3)
    ->for($user)
    ->create();
```

---

## Responses Redirects Redirecting to Named Routes Populating Parameters via Eloquent Models
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

#### Populating Parameters via Eloquent Models

If you are redirecting to a route with an "ID" parameter that is being populated from an Eloquent model, you may pass the model itself. The ID will be extracted automatically:

```php
// For a route with the following URI: /profile/{id}

return redirect()->route('profile', [$user]);
```

If you would like to customize the value that is placed in the route parameter, you can specify the column in the route parameter definition (`/profile/{id:slug}`) or you can override the `getRouteKey` method on your Eloquent model:

```php
/**
 * Get the value of the model's route key.
 */
public function getRouteKey(): mixed
{
    return $this->slug;
}
```

---

## Eloquent Factories
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

# Eloquent: Factories

- [Introduction](#introduction)
- [Defining Model Factories](#defining-model-factories)
    - [Generating Factories](#generating-factories)
    - [Factory States](#factory-states)
    - [Factory Callbacks](#factory-callbacks)
- [Creating Models Using Factories](#creating-models-using-factories)
    - [Instantiating Models](#instantiating-models)
    - [Persisting Models](#persisting-models)
    - [Sequences](#sequences)
- [Factory Relationships](#factory-relationships)
    - [Has Many Relationships](#has-many-relationships)
    - [Belongs To Relationships](#belongs-to-relationships)
    - [Many to Many Relationships](#many-to-many-relationships)
    - [Polymorphic Relationships](#polymorphic-relationships)
    - [Defining Relationships Within Factories](#defining-relationships-within-factories)
    - [Recycling an Existing Model for Relationships](#recycling-an-existing-model-for-relationships)

---

## Eloquent Factories Factory Relationships Polymorphic Relationships
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

### Polymorphic Relationships

[Polymorphic relationships](/docs/{{version}}/eloquent-relationships#polymorphic-relationships) may also be created using factories. Polymorphic "morph many" relationships are created in the same way as typical "has many" relationships. For example, if an `App\Models\Post` model has a `morphMany` relationship with an `App\Models\Comment` model:

```php
use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();
```

---

## Database Testing Available Assertions assertModelExists
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

#### assertModelExists

Assert that a given model or collection of models exist in the database:

```php
use App\Models\User;

$user = User::factory()->create();

$this->assertModelExists($user);
```

---

## Eloquent Factories Creating Models Using Factories
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

## Creating Models Using Factories

---

## Responses Creating Responses Eloquent Models and Collections
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

#### Eloquent Models and Collections

You may also return [Eloquent ORM](/docs/{{version}}/eloquent) models and collections directly from your routes and controllers. When you do, Laravel will automatically convert the models and collections to JSON responses while respecting the model's [hidden attributes](/docs/{{version}}/eloquent-serialization#hiding-attributes-from-json):

```php
use App\Models\User;

Route::get('/user/{user}', function (User $user) {
    return $user;
});
```

---

## Database Testing Available Assertions assertModelMissing
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

#### assertModelMissing

Assert that a given model or collection of models do not exist in the database:

```php
use App\Models\User;

$user = User::factory()->create();

$user->delete();

$this->assertModelMissing($user);
```

---

## Pagination Basic Usage Paginating Eloquent Results
Pagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md

### Paginating Eloquent Results

You may also paginate [Eloquent](/docs/{{version}}/eloquent) queries. In this example, we will paginate the `App\Models\User` model and indicate that we plan to display 15 records per page. As you can see, the syntax is nearly identical to paginating query builder results:

```php
use App\Models\User;

$users = User::paginate(15);
```

Of course, you may call the `paginate` method after setting other constraints on the query, such as `where` clauses:

```php
$users = User::where('votes', '>', 100)->paginate(15);
```

You may also use the `simplePaginate` method when paginating Eloquent models:

```php
$users = User::where('votes', '>', 100)->simplePaginate(15);
```

Similarly, you may use the `cursorPaginate` method to cursor paginate Eloquent models:

```php
$users = User::where('votes', '>', 100)->cursorPaginate(15);
```

---

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

# Database Testing

- [Introduction](#introduction)
    - [Resetting the Database After Each Test](#resetting-the-database-after-each-test)
- [Model Factories](#model-factories)
- [Running Seeders](#running-seeders)
- [Available Assertions](#available-assertions)

---

## Eloquent Events Muting Events Saving a Single Model Without Events
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

#### Saving a Single Model Without Events

Sometimes you may wish to "save" a given model without dispatching any events. You may accomplish this using the `saveQuietly` method:

```php
$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();
```

You may also "update", "delete", "soft delete", "restore", and "replicate" a given model without dispatching any events:

```php
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();
```

---

## Pagination Basic Usage Paginating Eloquent Results Multiple Paginator Instances per Page
Pagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md

#### Multiple Paginator Instances per Page

Sometimes you may need to render two separate paginators on a single screen that is rendered by your application. However, if both paginator instances use the `page` query string parameter to store the current page, the two paginators will conflict. To resolve this conflict, you may pass the name of the query string parameter you wish to use to store the paginator's current page via the third argument provided to the `paginate`, `simplePaginate`, and `cursorPaginate` methods:

```php
use App\Models\User;

$users = User::where('votes', '>', 100)->paginate(
    $perPage = 15, $columns = ['*'], $pageName = 'users'
);
```

---

## Eloquent Events Muting Events
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

### Muting Events

You may occasionally need to temporarily "mute" all events fired by a model. You may achieve this using the `withoutEvents` method. The `withoutEvents` method accepts a closure as its only argument. Any code executed within this closure will not dispatch model events, and any value returned by the closure will be returned by the `withoutEvents` method:

```php
use App\Models\User;

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});
```

---

## Eloquent Factories Factory Relationships Polymorphic Relationships Morph To Relationships
Eloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md

#### Morph To Relationships

Magic methods may not be used to create `morphTo` relationships. Instead, the `for` method must be used directly and the name of the relationship must be explicitly provided. For example, imagine that the `Comment` model has a `commentable` method that defines a `morphTo` relationship. In this situation, we may create three comments that belong to a single post by using the `for` method directly:

```php
$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();
```

---

## Eloquent Events Observers Observers and Database Transactions
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

#### Observers and Database Transactions

When models are being created within a database transaction, you may want to instruct an observer to only execute its event handlers after the database transaction is committed. You may accomplish this by implementing the `ShouldHandleEventsAfterCommit` interface on your observer. If a database transaction is not in progress, the event handlers will execute immediately:

```php
<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        // ...
    }
}
```

---

## Eloquent Events Using Closures
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

### Using Closures

Instead of using custom event classes, you may register closures that execute when various model events are dispatched. Typically, you should register these closures in the `booted` method of your model:

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // ...
        });
    }
}
```

If needed, you may utilize [queueable anonymous event listeners](/docs/{{version}}/events#queuable-anonymous-event-listeners) when registering model events. This will instruct Laravel to execute the model event listener in the background using your application's [queue](/docs/{{version}}/queues):

```php
use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // ...
}));
```

---

## Pennant Adding Custom Pennant Drivers Defining Features Externally
Pennant | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pennant.md

### Defining Features Externally

If your driver is a wrapper around a third-party feature flag platform, you will likely define features on the platform rather than using Pennant's `Feature::define` method. If that is the case, your custom driver should also implement the `Laravel\Pennant\Contracts\DefinesFeaturesExternally` interface:

```php
<?php

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;

class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
    /**
     * Get the features defined for the given scope.
     */
    public function definedFeaturesForScope(mixed $scope): array {}

    /* ... */
}
```

The `definedFeaturesForScope` method should return a list of feature names defined for the provided scope.

---

## Pennant Rich Feature Values
Pennant | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pennant.md

## Rich Feature Values

Until now, we have primarily shown features as being in a binary state, meaning they are either "active" or "inactive", but Pennant also allows you to store rich values as well.

For example, imagine you are testing three new colors for the "Buy now" button of your application. Instead of returning `true` or `false` from the feature definition, you may instead return a string:

```php
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;

Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));
```

You may retrieve the value of the `purchase-button` feature using the `value` method:

```php
$color = Feature::value('purchase-button');
```

Pennant's included Blade directive also makes it easy to conditionally render content based on the current value of the feature:

```blade
@feature('purchase-button', 'blue-sapphire')
    <!-- 'blue-sapphire' is active -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' is active -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' is active -->
@endfeature
```

> [!NOTE]
> When using rich values, it is important to know that a feature is considered "active" when it has any value other than `false`.

When calling the [conditional `when`](#conditional-execution) method, the feature's rich value will be provided to the first closure:

```php
Feature::when('purchase-button',
    fn ($color) => /* ... */,
    fn () => /* ... */,
);
```

Likewise, when calling the conditional `unless` method, the feature's rich value will be provided to the optional second closure:

```php
Feature::unless('purchase-button',
    fn () => /* ... */,
    fn ($color) => /* ... */,
);
```

_[Truncated]_


output:
Wall time: 3.3086 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Database Testing Model Factories\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n## Model Factories\n\nWhen testing, you may need to insert a few records into your database before executing your test. Instead of manually specifying the value of each column when you create this test data, Laravel allows you to define a set of default attributes for each of your [Eloquent models](/docs/{{version}}/eloquent) using [model factories](/docs/{{version}}/eloquent-factories).\n\nTo learn more about creating and utilizing model factories to create models, please consult the complete [model factory documentation](/docs/{{version}}/eloquent-factories). Once you have defined a model factory, you may utilize the factory within your test to create models:\n\n```php tab=Pest\nuse App\\Models\\User;\n\ntest('models can be instantiated', function () {\n    $user = User::factory()->create();\n\n    // ...\n});\n```\n\n```php tab=PHPUnit\nuse App\\Models\\User;\n\npublic function test_models_can_be_instantiated(): void\n{\n    $user = User::factory()->create();\n\n    // ...\n}\n```\n\n---\n\n## Urls URLs for Named Routes Eloquent Models\nUrls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md\n\n\n#### Eloquent Models\n\nYou will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:\n\n```php\necho route('post.show', ['post' => $post]);\n```\n\n---\n\n## Eloquent Factories Factory Relationships\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n## Factory Relationships\n\n---\n\n## Database Testing Introduction\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n## Introduction\n\nLaravel provides a variety of helpful tools and assertions to make it easier to test your database driven applications. In addition, Laravel model factories and seeders make it painless to create test database records using your application's Eloquent models and relationships. We'll discuss all of these powerful features in the following documentation.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Models and UUIDv7\n\n**Likelihood Of Impact: Medium**\n\nThe `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids; // [tl! remove]\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasVersion4Uuids as HasUuids; // [tl! add]\n```\n\nThe `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.\n\n---\n\n## Eloquent Factories Factory Relationships Defining Relationships Within Factories\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n### Defining Relationships Within Factories\n\nTo define a relationship within your model factory, you will typically assign a new factory instance to the foreign key of the relationship. This is normally done for the \"inverse\" relationships such as `belongsTo` and `morphTo` relationships. For example, if you would like to create a new user when creating a post, you may do the following:\n\n```php\nuse App\\Models\\User;\n\n/**\n * Define the model's default state.\n *\n * @return array<string, mixed>\n */\npublic function definition(): array\n{\n    return [\n        'user_id' => User::factory(),\n        'title' => fake()->title(),\n        'content' => fake()->paragraph(),\n    ];\n}\n```\n\nIf the relationship's columns depend on the factory that defines it you may assign a closure to an attribute. The closure will receive the factory's evaluated attribute array:\n\n```php\n/**\n * Define the model's default state.\n *\n * @return array<string, mixed>\n */\npublic function definition(): array\n{\n    return [\n        'user_id' => User::factory(),\n        'user_type' => function (array $attributes) {\n            return User::find($attributes['user_id'])->type;\n        },\n        'title' => fake()->title(),\n        'content' => fake()->paragraph(),\n    ];\n}\n```\n\n---\n\n## Scout Searching Where Clauses Customizing the Eloquent Results Query\nScout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md\n\n\n#### Customizing the Eloquent Results Query\n\nAfter Scout retrieves a list of matching Eloquent models from your application's search engine, Eloquent is used to retrieve all of the matching models by their primary keys. You may customize this query by invoking the `query` method. The `query` method accepts a closure that will receive the Eloquent query builder instance as an argument:\n\n```php\nuse App\\Models\\Order;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n$orders = Order::search('Star Trek')\n    ->query(fn (Builder $query) => $query->with('invoices'))\n    ->get();\n```\n\nWhen using a third-party engine, this callback is invoked after the relevant models have already been retrieved from the search engine, so it should not be used for \"filtering\" results — use [Scout where clauses](#where-clauses) instead. However, when using the database engine, the `query` method's constraints are applied directly to the database query, so you may use it for filtering as well.\n\n---\n\n## Eloquent Factories Factory Relationships Belongs To Relationships\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n### Belongs To Relationships\n\nNow that we have explored how to build \"has many\" relationships using factories, let's explore the inverse of the relationship. The `for` method may be used to define the parent model that factory created models belong to. For example, we can create three `App\\Models\\Post` model instances that belong to a single user:\n\n```php\nuse App\\Models\\Post;\nuse App\\Models\\User;\n\n$posts = Post::factory()\n    ->count(3)\n    ->for(User::factory()->state([\n        'name' => 'Jessica Archer',\n    ]))\n    ->create();\n```\n\nIf you already have a parent model instance that should be associated with the models you are creating, you may pass the model instance to the `for` method:\n\n```php\n$user = User::factory()->create();\n\n$posts = Post::factory()\n    ->count(3)\n    ->for($user)\n    ->create();\n```\n\n---\n\n## Responses Redirects Redirecting to Named Routes Populating Parameters via Eloquent Models\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Populating Parameters via Eloquent Models\n\nIf you are redirecting to a route with an \"ID\" parameter that is being populated from an Eloquent model, you may pass the model itself. The ID will be extracted automatically:\n\n```php\n// For a route with the following URI: /profile/{id}\n\nreturn redirect()->route('profile', [$user]);\n```\n\nIf you would like to customize the value that is placed in the route parameter, you can specify the column in the route parameter definition (`/profile/{id:slug}`) or you can override the `getRouteKey` method on your Eloquent model:\n\n```php\n/**\n * Get the value of the model's route key.\n */\npublic function getRouteKey(): mixed\n{\n    return $this->slug;\n}\n```\n\n---\n\n## Eloquent Factories\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n# Eloquent: Factories\n\n- [Introduction](#introduction)\n- [Defining Model Factories](#defining-model-factories)\n    - [Generating Factories](#generating-factories)\n    - [Factory States](#factory-states)\n    - [Factory Callbacks](#factory-callbacks)\n- [Creating Models Using Factories](#creating-models-using-factories)\n    - [Instantiating Models](#instantiating-models)\n    - [Persisting Models](#persisting-models)\n    - [Sequences](#sequences)\n- [Factory Relationships](#factory-relationships)\n    - [Has Many Relationships](#has-many-relationships)\n    - [Belongs To Relationships](#belongs-to-relationships)\n    - [Many to Many Relationships](#many-to-many-relationships)\n    - [Polymorphic Relationships](#polymorphic-relationships)\n    - [Defining Relationships Within Factories](#defining-relationships-within-factories)\n    - [Recycling an Existing Model for Relationships](#recycling-an-existing-model-for-relationships)\n\n---\n\n## Eloquent Factories Factory Relationships Polymorphic Relationships\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n### Polymorphic Relationships\n\n[Polymorphic relationships](/docs/{{version}}/eloquent-relationships#polymorphic-relationships) may also be created using factories. Polymorphic \"morph many\" relationships are created in the same way as typical \"has many\" relationships. For example, if an `App\\Models\\Post` model has a `morphMany` relationship with an `App\\Models\\Comment` model:\n\n```php\nuse App\\Models\\Post;\n\n$post = Post::factory()->hasComments(3)->create();\n```\n\n---\n\n## Database Testing Available Assertions assertModelExists\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n#### assertModelExists\n\nAssert that a given model or collection of models exist in the database:\n\n```php\nuse App\\Models\\User;\n\n$user = User::factory()->create();\n\n$this->assertModelExists($user);\n```\n\n---\n\n## Eloquent Factories Creating Models Using Factories\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n## Creating Models Using Factories\n\n---\n\n## Responses Creating Responses Eloquent Models and Collections\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Eloquent Models and Collections\n\nYou may also return [Eloquent ORM](/docs/{{version}}/eloquent) models and collections directly from your routes and controllers. When you do, Laravel will automatically convert the models and collections to JSON responses while respecting the model's [hidden attributes](/docs/{{version}}/eloquent-serialization#hiding-attributes-from-json):\n\n```php\nuse App\\Models\\User;\n\nRoute::get('/user/{user}', function (User $user) {\n    return $user;\n});\n```\n\n---\n\n## Database Testing Available Assertions assertModelMissing\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n#### assertModelMissing\n\nAssert that a given model or collection of models do not exist in the database:\n\n```php\nuse App\\Models\\User;\n\n$user = User::factory()->create();\n\n$user->delete();\n\n$this->assertModelMissing($user);\n```\n\n---\n\n## Pagination Basic Usage Paginating Eloquent Results\nPagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md\n\n\n### Paginating Eloquent Results\n\nYou may also paginate [Eloquent](/docs/{{version}}/eloquent) queries. In this example, we will paginate the `App\\Models\\User` model and indicate that we plan to display 15 records per page. As you can see, the syntax is nearly identical to paginating query builder results:\n\n```php\nuse App\\Models\\User;\n\n$users = User::paginate(15);\n```\n\nOf course, you may call the `paginate` method after setting other constraints on the query, such as `where` clauses:\n\n```php\n$users = User::where('votes', '>', 100)->paginate(15);\n```\n\nYou may also use the `simplePaginate` method when paginating Eloquent models:\n\n```php\n$users = User::where('votes', '>', 100)->simplePaginate(15);\n```\n\nSimilarly, you may use the `cursorPaginate` method to cursor paginate Eloquent models:\n\n```php\n$users = User::where('votes', '>', 100)->cursorPaginate(15);\n```\n\n---\n\n## Database Testing\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n# Database Testing\n\n- [Introduction](#introduction)\n    - [Resetting the Database After Each Test](#resetting-the-database-after-each-test)\n- [Model Factories](#model-factories)\n- [Running Seeders](#running-seeders)\n- [Available Assertions](#available-assertions)\n\n---\n\n## Eloquent Events Muting Events Saving a Single Model Without Events\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n#### Saving a Single Model Without Events\n\nSometimes you may wish to \"save\" a given model without dispatching any events. You may accomplish this using the `saveQuietly` method:\n\n```php\n$user = User::findOrFail(1);\n\n$user->name = 'Victoria Faith';\n\n$user->saveQuietly();\n```\n\nYou may also \"update\", \"delete\", \"soft delete\", \"restore\", and \"replicate\" a given model without dispatching any events:\n\n```php\n$user->deleteQuietly();\n$user->forceDeleteQuietly();\n$user->restoreQuietly();\n```\n\n---\n\n## Pagination Basic Usage Paginating Eloquent Results Multiple Paginator Instances per Page\nPagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md\n\n\n#### Multiple Paginator Instances per Page\n\nSometimes you may need to render two separate paginators on a single screen that is rendered by your application. However, if both paginator instances use the `page` query string parameter to store the current page, the two paginators will conflict. To resolve this conflict, you may pass the name of the query string parameter you wish to use to store the paginator's current page via the third argument provided to the `paginate`, `simplePaginate`, and `cursorPaginate` methods:\n\n```php\nuse App\\Models\\User;\n\n$users = User::where('votes', '>', 100)->paginate(\n    $perPage = 15, $columns = ['*'], $pageName = 'users'\n);\n```\n\n---\n\n## Eloquent Events Muting Events\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n### Muting Events\n\nYou may occasionally need to temporarily \"mute\" all events fired by a model. You may achieve this using the `withoutEvents` method. The `withoutEvents` method accepts a closure as its only argument. Any code executed within this closure will not dispatch model events, and any value returned by the closure will be returned by the `withoutEvents` method:\n\n```php\nuse App\\Models\\User;\n\n$user = User::withoutEvents(function () {\n    User::findOrFail(1)->delete();\n\n    return User::find(2);\n});\n```\n\n---\n\n## Eloquent Factories Factory Relationships Polymorphic Relationships Morph To Relationships\nEloquent Factories | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-factories.md\n\n\n#### Morph To Relationships\n\nMagic methods may not be used to create `morphTo` relationships. Instead, the `for` method must be used directly and the name of the relationship must be explicitly provided. For example, imagine that the `Comment` model has a `commentable` method that defines a `morphTo` relationship. In this situation, we may create three comments that belong to a single post by using the `for` method directly:\n\n```php\n$comments = Comment::factory()->count(3)->for(\n    Post::factory(), 'commentable'\n)->create();\n```\n\n---\n\n## Eloquent Events Observers Observers and Database Transactions\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n#### Observers and Database Transactions\n\nWhen models are being created within a database transaction, you may want to instruct an observer to only execute its event handlers after the database transaction is committed. You may accomplish this by implementing the `ShouldHandleEventsAfterCommit` interface on your observer. If a database transaction is not in progress, the event handlers will execute immediately:\n\n```php\n<?php\n\nnamespace App\\Observers;\n\nuse App\\Models\\User;\nuse Illuminate\\Contracts\\Events\\ShouldHandleEventsAfterCommit;\n\nclass UserObserver implements ShouldHandleEventsAfterCommit\n{\n    /**\n     * Handle the User \"created\" event.\n     */\n    public function created(User $user): void\n    {\n        // ...\n    }\n}\n```\n\n---\n\n## Eloquent Events Using Closures\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n### Using Closures\n\nInstead of using custom event classes, you may register closures that execute when various model events are dispatched. Typically, you should register these closures in the `booted` method of your model:\n\n```php\n<?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass User extends Model\n{\n    /**\n     * The \"booted\" method of the model.\n     */\n    protected static function booted(): void\n    {\n        static::created(function (User $user) {\n            // ...\n        });\n    }\n}\n```\n\nIf needed, you may utilize [queueable anonymous event listeners](/docs/{{version}}/events#queuable-anonymous-event-listeners) when registering model events. This will instruct Laravel to execute the model event listener in the background using your application's [queue](/docs/{{version}}/queues):\n\n```php\nuse function Illuminate\\Events\\queueable;\n\nstatic::created(queueable(function (User $user) {\n    // ...\n}));\n```\n\n---\n\n## Pennant Adding Custom Pennant Drivers Defining Features Externally\nPennant | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pennant.md\n\n\n### Defining Features Externally\n\nIf your driver is a wrapper around a third-party feature flag platform, you will likely define features on the platform rather than using Pennant's `Feature::define` method. If that is the case, your custom driver should also implement the `Laravel\\Pennant\\Contracts\\DefinesFeaturesExternally` interface:\n\n```php\n<?php\n\nnamespace App\\Extensions;\n\nuse Laravel\\Pennant\\Contracts\\Driver;\nuse Laravel\\Pennant\\Contracts\\DefinesFeaturesExternally;\n\nclass FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally\n{\n    /**\n     * Get the features defined for the given scope.\n     */\n    public function definedFeaturesForScope(mixed $scope): array {}\n\n    /* ... */\n}\n```\n\nThe `definedFeaturesForScope` method should return a list of feature names defined for the provided scope.\n\n---\n\n## Pennant Rich Feature Values\nPennant | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pennant.md\n\n\n## Rich Feature Values\n\nUntil now, we have primarily shown features as being in a binary state, meaning they are either \"active\" or \"inactive\", but Pennant also allows you to store rich values as well.\n\nFor example, imagine you are testing three new colors for the \"Buy now\" button of your application. Instead of returning `true` or `false` from the feature definition, you may instead return a string:\n\n```php\nuse Illuminate\\Support\\Arr;\nuse Laravel\\Pennant\\Feature;\n\nFeature::define('purchase-button', fn (User $user) => Arr::random([\n    'blue-sapphire',\n    'seafoam-green',\n    'tart-orange',\n]));\n```\n\nYou may retrieve the value of the `purchase-button` feature using the `value` method:\n\n```php\n$color = Feature::value('purchase-button');\n```\n\nPennant's included Blade directive also makes it easy to conditionally render content based on the current value of the feature:\n\n```blade\n@feature('purchase-button', 'blue-sapphire')\n    <!-- 'blue-sapphire' is active -->\n@elsefeature('purchase-button', 'seafoam-green')\n    <!-- 'seafoam-green' is active -->\n@elsefeature('purchase-button', 'tart-orange')\n    <!-- 'tart-orange' is active -->\n@endfeature\n```\n\n> [!NOTE]\n> When using rich values, it is important to know that a feature is considered \"active\" when it has any value other than `false`.\n\nWhen calling the [conditional `when`](#conditional-execution) method, the feature's rich value will be provided to the first closure:\n\n```php\nFeature::when('purchase-button',\n    fn ($color) => /* ... */,\n    fn () => /* ... */,\n);\n```\n\nLikewise, when calling the conditional `unless` method, the feature's rich value will be provided to the optional second closure:\n\n```php\nFeature::unless('purchase-button',\n    fn () => /* ... */,\n    fn ($color) => /* ... */,\n);\n```\n\n_[Truncated]_"}]


message: The versioned docs confirm the Laravel 12 pattern for `booted()` model event closures and `withoutEvents()` for factories that need to bypass those events. I’m checking the current catalog files now so the patch stays aligned with the existing service and test style.

phase: commentary


type: message

role: assistant

text: The versioned docs confirm the Laravel 12 pattern for `booted()` model event closures and `withoutEvents()` for factories that need to bypass those events. I’m checking the current catalog files now so the patch stays aligned with the existing service and test style.


cmd: sed -n '1,240p' app/Models/ProductVariant.php


cmd: sed -n '1,260p' app/Services/ProductService.php


cmd: sed -n '1,260p' app/Services/VariantMatrixService.php


cmd: sed -n '1,220p' database/factories/InventoryItemFactory.php


cmd: sed -n '1,240p' database/factories/ProductFactory.php


command: sed -n '1,240p' database/factories/ProductFactory.php

ProductFactory.php

type: read

aggregated_output:
<?php

namespace Database\Factories;

use App\Enums\ProductStatus;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(3, true));

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'status' => ProductStatus::Active,
            'description_html' => '<p>'.fake()->paragraph().'</p>',
            'vendor' => fake()->company(),
            'product_type' => fake()->randomElement(['Shirts', 'Pants', 'Shoes', 'Accessories', 'Electronics', 'Books']),
            'tags' => fake()->randomElements(['new', 'sale', 'trending', 'popular', 'limited'], fake()->numberBetween(1, 3)),
            'published_at' => now(),
        ];
    }

    public function draft(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Draft,
            'published_at' => null,
        ]);
    }

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

    public function withVariants(int $count): static
    {
        return $this->afterCreating(function ($product) use ($count): void {
            ProductVariant::factory()
                ->count($count)
                ->for($product)
                ->create();
        });
    }

    public function withDefaultVariant(int $price = 2499): static
    {
        return $this->afterCreating(function ($product) use ($price): void {
            $variant = ProductVariant::factory()
                ->default()
                ->for($product)
                ->create([
                    'price_amount' => $price,
                    'currency' => $product->store->default_currency,
                ]);

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

status: completed


command: sed -n '1,240p' app/Models/ProductVariant.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    protected static function booted(): void
    {
        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = $variant->product()->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => 'deny',
            ]);
        });
    }

    /**
     * @return BelongsTo<Product, $this>
     */
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    /**
     * @return HasOne<InventoryItem, $this>
     */
    public function inventoryItem(): HasOne
    {
        return $this->hasOne(InventoryItem::class, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}


command: sed -n '1,260p' app/Services/ProductService.php

aggregated_output:
<?php

namespace App\Services;

use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Events\ProductStatusChanged;
use App\Exceptions\InvalidProductTransitionException;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Support\HandleGenerator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;

class ProductService
{
    public function __construct(
        private readonly HandleGenerator $handles,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(Store $store, array $data): Product
    {
        return DB::transaction(function () use ($store, $data): Product {
            $product = Product::query()->create([
                ...Arr::only($data, [
                    'title',
                    'status',
                    'description_html',
                    'vendor',
                    'product_type',
                    'tags',
                    'published_at',
                ]),
                'store_id' => $store->getKey(),
                'handle' => $data['handle'] ?? $this->handles->generate($data['title'], 'products', $store->getKey()),
            ]);

            foreach ($data['options'] ?? [] as $optionData) {
                $option = $product->options()->create(Arr::only($optionData, ['name', 'position']));

                foreach ($optionData['values'] ?? [] as $valueData) {
                    $option->values()->create(Arr::only($valueData, ['value', 'position']));
                }
            }

            $variants = $data['variants'] ?? [];

            if ($variants === []) {
                $variants = [[
                    'sku' => $data['sku'] ?? null,
                    'price_amount' => $data['price_amount'] ?? 0,
                    'currency' => $store->default_currency,
                    'is_default' => true,
                    'position' => 0,
                ]];
            }

            foreach ($variants as $variantData) {
                $this->createVariant($product, $store, $variantData);
            }

            return $product->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(Product $product, array $data): Product
    {
        return DB::transaction(function () use ($product, $data): Product {
            $storeId = $product->store_id;

            $product->fill(Arr::only($data, [
                'title',
                'status',
                'description_html',
                'vendor',
                'product_type',
                'tags',
                'published_at',
            ]));

            if (array_key_exists('handle', $data)) {
                $product->handle = $data['handle'];
            } elseif (array_key_exists('title', $data)) {
                $product->handle = $this->handles->generate($data['title'], 'products', $storeId, $product->getKey());
            }

            $product->save();

            return $product->refresh();
        });
    }

    public function transitionStatus(Product $product, ProductStatus $newStatus): void
    {
        $currentStatus = $this->productStatus($product);

        if ($currentStatus === $newStatus) {
            return;
        }

        if ($newStatus === ProductStatus::Active) {
            $this->assertCanActivate($product);
        }

        if ($newStatus === ProductStatus::Draft && $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
        }

        if (! $this->isAllowedTransition($currentStatus, $newStatus)) {
            throw InvalidProductTransitionException::because("Cannot transition product from {$currentStatus->value} to {$newStatus->value}.");
        }

        $product->forceFill([
            'status' => $newStatus,
            'published_at' => $newStatus === ProductStatus::Active
                ? ($product->published_at ?? now())
                : $product->published_at,
        ])->save();

        ProductStatusChanged::dispatch($product->refresh(), $currentStatus, $newStatus);
    }

    public function delete(Product $product): void
    {
        if ($this->productStatus($product) !== ProductStatus::Draft || $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Only draft products with no order history can be deleted.');
        }

        $product->delete();
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function createVariant(Product $product, Store $store, array $variantData): ProductVariant
    {
        $this->assertSkuIsUnique($store, $variantData['sku'] ?? null);

        $variant = $product->variants()->create([
            ...Arr::only($variantData, [
                'sku',
                'barcode',
                'price_amount',
                'compare_at_amount',
                'currency',
                'weight_g',
                'requires_shipping',
                'is_default',
                'position',
                'status',
            ]),
            'currency' => $variantData['currency'] ?? $store->default_currency,
            'status' => $variantData['status'] ?? VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $store->getKey(),
                'quantity_on_hand' => $variantData['quantity_on_hand'] ?? 0,
                'quantity_reserved' => 0,
                'policy' => $variantData['inventory_policy'] ?? 'deny',
            ],
        );

        return $variant;
    }

    private function assertCanActivate(Product $product): void
    {
        if (trim((string) $product->title) === '') {
            throw InvalidProductTransitionException::because('A product title is required before activation.');
        }

        if (! $product->variants()->where('price_amount', '>', 0)->exists()) {
            throw InvalidProductTransitionException::because('At least one priced variant is required before activation.');
        }
    }

    private function isAllowedTransition(ProductStatus $from, ProductStatus $to): bool
    {
        return match ($from) {
            ProductStatus::Draft => in_array($to, [ProductStatus::Active, ProductStatus::Archived], true),
            ProductStatus::Active => in_array($to, [ProductStatus::Archived, ProductStatus::Draft], true),
            ProductStatus::Archived => in_array($to, [ProductStatus::Active, ProductStatus::Draft], true),
        };
    }

    private function hasOrderLineReferences(Product $product): bool
    {
        if (! Schema::hasTable('order_lines')) {
            return false;
        }

        $variantIds = $product->variants()->pluck('id');

        if ($variantIds->isEmpty()) {
            return false;
        }

        return DB::table('order_lines')
            ->whereIn('variant_id', $variantIds)
            ->exists();
    }

    private function assertSkuIsUnique(Store $store, ?string $sku, ?int $excludeVariantId = null): void
    {
        if ($sku === null || trim($sku) === '') {
            return;
        }

        $query = ProductVariant::query()
            ->where('sku', $sku)
            ->whereHas('product', fn ($query) => $query->where('store_id', $store->getKey()));

        if ($excludeVariantId !== null) {
            $query->whereKeyNot($excludeVariantId);
        }

        if ($query->exists()) {
            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
        }
    }

    private function productStatus(Product $product): ProductStatus
    {
        return $product->status instanceof ProductStatus
            ? $product->status
            : ProductStatus::from($product->status);
    }
}


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

namespace App\Models;

use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    protected static function booted(): void
    {
        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = $variant->product()->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => 'deny',
            ]);
        });
    }

    /**
     * @return BelongsTo<Product, $this>
     */
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    /**
     * @return HasOne<InventoryItem, $this>
     */
    public function inventoryItem(): HasOne
    {
        return $this->hasOne(InventoryItem::class, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}


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

namespace App\Services;

use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Events\ProductStatusChanged;
use App\Exceptions\InvalidProductTransitionException;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Support\HandleGenerator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;

class ProductService
{
    public function __construct(
        private readonly HandleGenerator $handles,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function create(Store $store, array $data): Product
    {
        return DB::transaction(function () use ($store, $data): Product {
            $product = Product::query()->create([
                ...Arr::only($data, [
                    'title',
                    'status',
                    'description_html',
                    'vendor',
                    'product_type',
                    'tags',
                    'published_at',
                ]),
                'store_id' => $store->getKey(),
                'handle' => $data['handle'] ?? $this->handles->generate($data['title'], 'products', $store->getKey()),
            ]);

            foreach ($data['options'] ?? [] as $optionData) {
                $option = $product->options()->create(Arr::only($optionData, ['name', 'position']));

                foreach ($optionData['values'] ?? [] as $valueData) {
                    $option->values()->create(Arr::only($valueData, ['value', 'position']));
                }
            }

            $variants = $data['variants'] ?? [];

            if ($variants === []) {
                $variants = [[
                    'sku' => $data['sku'] ?? null,
                    'price_amount' => $data['price_amount'] ?? 0,
                    'currency' => $store->default_currency,
                    'is_default' => true,
                    'position' => 0,
                ]];
            }

            foreach ($variants as $variantData) {
                $this->createVariant($product, $store, $variantData);
            }

            return $product->refresh();
        });
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(Product $product, array $data): Product
    {
        return DB::transaction(function () use ($product, $data): Product {
            $storeId = $product->store_id;

            $product->fill(Arr::only($data, [
                'title',
                'status',
                'description_html',
                'vendor',
                'product_type',
                'tags',
                'published_at',
            ]));

            if (array_key_exists('handle', $data)) {
                $product->handle = $data['handle'];
            } elseif (array_key_exists('title', $data)) {
                $product->handle = $this->handles->generate($data['title'], 'products', $storeId, $product->getKey());
            }

            $product->save();

            return $product->refresh();
        });
    }

    public function transitionStatus(Product $product, ProductStatus $newStatus): void
    {
        $currentStatus = $this->productStatus($product);

        if ($currentStatus === $newStatus) {
            return;
        }

        if ($newStatus === ProductStatus::Active) {
            $this->assertCanActivate($product);
        }

        if ($newStatus === ProductStatus::Draft && $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
        }

        if (! $this->isAllowedTransition($currentStatus, $newStatus)) {
            throw InvalidProductTransitionException::because("Cannot transition product from {$currentStatus->value} to {$newStatus->value}.");
        }

        $product->forceFill([
            'status' => $newStatus,
            'published_at' => $newStatus === ProductStatus::Active
                ? ($product->published_at ?? now())
                : $product->published_at,
        ])->save();

        ProductStatusChanged::dispatch($product->refresh(), $currentStatus, $newStatus);
    }

    public function delete(Product $product): void
    {
        if ($this->productStatus($product) !== ProductStatus::Draft || $this->hasOrderLineReferences($product)) {
            throw InvalidProductTransitionException::because('Only draft products with no order history can be deleted.');
        }

        $product->delete();
    }

    /**
     * @param  array<string, mixed>  $variantData
     */
    private function createVariant(Product $product, Store $store, array $variantData): ProductVariant
    {
        $this->assertSkuIsUnique($store, $variantData['sku'] ?? null);

        $variant = $product->variants()->create([
            ...Arr::only($variantData, [
                'sku',
                'barcode',
                'price_amount',
                'compare_at_amount',
                'currency',
                'weight_g',
                'requires_shipping',
                'is_default',
                'position',
                'status',
            ]),
            'currency' => $variantData['currency'] ?? $store->default_currency,
            'status' => $variantData['status'] ?? VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $store->getKey(),
                'quantity_on_hand' => $variantData['quantity_on_hand'] ?? 0,
                'quantity_reserved' => 0,
                'policy' => $variantData['inventory_policy'] ?? 'deny',
            ],
        );

        return $variant;
    }

    private function assertCanActivate(Product $product): void
    {
        if (trim((string) $product->title) === '') {
            throw InvalidProductTransitionException::because('A product title is required before activation.');
        }

        if (! $product->variants()->where('price_amount', '>', 0)->exists()) {
            throw InvalidProductTransitionException::because('At least one priced variant is required before activation.');
        }
    }

    private function isAllowedTransition(ProductStatus $from, ProductStatus $to): bool
    {
        return match ($from) {
            ProductStatus::Draft => in_array($to, [ProductStatus::Active, ProductStatus::Archived], true),
            ProductStatus::Active => in_array($to, [ProductStatus::Archived, ProductStatus::Draft], true),
            ProductStatus::Archived => in_array($to, [ProductStatus::Active, ProductStatus::Draft], true),
        };
    }

    private function hasOrderLineReferences(Product $product): bool
    {
        if (! Schema::hasTable('order_lines')) {
            return false;
        }

        $variantIds = $product->variants()->pluck('id');

        if ($variantIds->isEmpty()) {
            return false;
        }

        return DB::table('order_lines')
            ->whereIn('variant_id', $variantIds)
            ->exists();
    }

    private function assertSkuIsUnique(Store $store, ?string $sku, ?int $excludeVariantId = null): void
    {
        if ($sku === null || trim($sku) === '') {
            return;
        }

        $query = ProductVariant::query()
            ->where('sku', $sku)
            ->whereHas('product', fn ($query) => $query->where('store_id', $store->getKey()));

        if ($excludeVariantId !== null) {
            $query->whereKeyNot($excludeVariantId);
        }

        if ($query->exists()) {
            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
        }
    }

    private function productStatus(Product $product): ProductStatus
    {
        return $product->status instanceof ProductStatus
            ? $product->status
            : ProductStatus::from($product->status);
    }
}


command: sed -n '1,220p' database/factories/InventoryItemFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Enums\InventoryPolicy;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\InventoryItem>
 */
class InventoryItemFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'variant_id' => fn (): int => ProductVariant::withoutEvents(
                fn (): int => ProductVariant::factory()->create()->getKey(),
            ),
            'store_id' => fn (array $attributes): mixed => ProductVariant::query()
                ->with('product')
                ->find($attributes['variant_id'])
                ?->product
                ?->store_id ?? Store::factory(),
            'quantity_on_hand' => fake()->numberBetween(0, 100),
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ];
    }

    public function outOfStock(): static
    {
        return $this->state(fn (array $attributes): array => [
            'quantity_on_hand' => 0,
            'quantity_reserved' => 0,
        ]);
    }

    public function continuePolicy(): static
    {
        return $this->state(fn (array $attributes): array => [
            'policy' => InventoryPolicy::Continue,
        ]);
    }

    public function lowStock(): static
    {
        return $this->state(fn (array $attributes): array => [
            'quantity_on_hand' => fake()->numberBetween(1, 3),
            'quantity_reserved' => 0,
        ]);
    }
}


command: sed -n '1,260p' app/Services/VariantMatrixService.php

aggregated_output:
<?php

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class VariantMatrixService
{
    public function rebuildMatrix(Product $product): void
    {
        DB::transaction(function () use ($product): void {
            $product->load(['options.values', 'variants.optionValues']);

            $valueGroups = $product->options
                ->sortBy('position')
                ->map(fn ($option) => $option->values->sortBy('position')->values())
                ->filter(fn (Collection $values): bool => $values->isNotEmpty())
                ->values();

            if ($valueGroups->isEmpty()) {
                $this->ensureDefaultVariant($product);

                return;
            }

            $desiredCombinations = $this->cartesianProduct($valueGroups);
            $variantsByCombination = $product->variants->keyBy(function (ProductVariant $variant): string {
                return $this->combinationKey($variant->optionValues->pluck('id')->all());
            });

            $template = $product->variants->sortBy('position')->first();

            foreach ($desiredCombinations as $position => $combination) {
                $key = $this->combinationKey($combination);

                if ($variantsByCombination->has($key)) {
                    continue;
                }

                $variant = $this->createVariantFromTemplate($product, $template, $position);
                $variant->optionValues()->sync($combination);
            }

            $desiredKeys = collect($desiredCombinations)
                ->map(fn (array $combination): string => $this->combinationKey($combination))
                ->all();

            foreach ($product->variants as $variant) {
                $key = $this->combinationKey($variant->optionValues->pluck('id')->all());

                if (in_array($key, $desiredKeys, true)) {
                    continue;
                }

                if ($this->variantHasOrderLines($variant)) {
                    $variant->forceFill(['status' => VariantStatus::Archived])->save();
                } else {
                    $variant->delete();
                }
            }
        });
    }

    private function ensureDefaultVariant(Product $product): void
    {
        if ($product->variants()->exists()) {
            return;
        }

        $variant = $product->variants()->create([
            'price_amount' => 0,
            'currency' => $this->store($product)->default_currency,
            'requires_shipping' => true,
            'is_default' => true,
            'position' => 0,
            'status' => VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $product->store_id,
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ],
        );
    }

    /**
     * @param  Collection<int, Collection<int, object>>  $groups
     * @return array<int, array<int, int>>
     */
    private function cartesianProduct(Collection $groups): array
    {
        return $groups->reduce(
            function (array $carry, Collection $group): array {
                $result = [];

                foreach ($carry as $combination) {
                    foreach ($group as $value) {
                        $result[] = [...$combination, $value->getKey()];
                    }
                }

                return $result;
            },
            [[]],
        );
    }

    /**
     * @param  array<int, int|string>  $ids
     */
    private function combinationKey(array $ids): string
    {
        sort($ids);

        return implode(':', $ids);
    }

    private function createVariantFromTemplate(Product $product, ?ProductVariant $template, int $position): ProductVariant
    {
        $variant = $product->variants()->create([
            'price_amount' => $template?->price_amount ?? 0,
            'compare_at_amount' => $template?->compare_at_amount,
            'currency' => $template?->currency ?? $this->store($product)->default_currency,
            'weight_g' => $template?->weight_g,
            'requires_shipping' => $template?->requires_shipping ?? true,
            'is_default' => false,
            'position' => $position,
            'status' => VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $product->store_id,
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ],
        );

        return $variant;
    }

    private function variantHasOrderLines(ProductVariant $variant): bool
    {
        return Schema::hasTable('order_lines')
            && DB::table('order_lines')->where('variant_id', $variant->getKey())->exists();
    }

    private function store(Product $product): Store
    {
        return Store::query()->findOrFail($product->store_id);
    }
}


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

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class VariantMatrixService
{
    public function rebuildMatrix(Product $product): void
    {
        DB::transaction(function () use ($product): void {
            $product->load(['options.values', 'variants.optionValues']);

            $valueGroups = $product->options
                ->sortBy('position')
                ->map(fn ($option) => $option->values->sortBy('position')->values())
                ->filter(fn (Collection $values): bool => $values->isNotEmpty())
                ->values();

            if ($valueGroups->isEmpty()) {
                $this->ensureDefaultVariant($product);

                return;
            }

            $desiredCombinations = $this->cartesianProduct($valueGroups);
            $variantsByCombination = $product->variants->keyBy(function (ProductVariant $variant): string {
                return $this->combinationKey($variant->optionValues->pluck('id')->all());
            });

            $template = $product->variants->sortBy('position')->first();

            foreach ($desiredCombinations as $position => $combination) {
                $key = $this->combinationKey($combination);

                if ($variantsByCombination->has($key)) {
                    continue;
                }

                $variant = $this->createVariantFromTemplate($product, $template, $position);
                $variant->optionValues()->sync($combination);
            }

            $desiredKeys = collect($desiredCombinations)
                ->map(fn (array $combination): string => $this->combinationKey($combination))
                ->all();

            foreach ($product->variants as $variant) {
                $key = $this->combinationKey($variant->optionValues->pluck('id')->all());

                if (in_array($key, $desiredKeys, true)) {
                    continue;
                }

                if ($this->variantHasOrderLines($variant)) {
                    $variant->forceFill(['status' => VariantStatus::Archived])->save();
                } else {
                    $variant->delete();
                }
            }
        });
    }

    private function ensureDefaultVariant(Product $product): void
    {
        if ($product->variants()->exists()) {
            return;
        }

        $variant = $product->variants()->create([
            'price_amount' => 0,
            'currency' => $this->store($product)->default_currency,
            'requires_shipping' => true,
            'is_default' => true,
            'position' => 0,
            'status' => VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $product->store_id,
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ],
        );
    }

    /**
     * @param  Collection<int, Collection<int, object>>  $groups
     * @return array<int, array<int, int>>
     */
    private function cartesianProduct(Collection $groups): array
    {
        return $groups->reduce(
            function (array $carry, Collection $group): array {
                $result = [];

                foreach ($carry as $combination) {
                    foreach ($group as $value) {
                        $result[] = [...$combination, $value->getKey()];
                    }
                }

                return $result;
            },
            [[]],
        );
    }

    /**
     * @param  array<int, int|string>  $ids
     */
    private function combinationKey(array $ids): string
    {
        sort($ids);

        return implode(':', $ids);
    }

    private function createVariantFromTemplate(Product $product, ?ProductVariant $template, int $position): ProductVariant
    {
        $variant = $product->variants()->create([
            'price_amount' => $template?->price_amount ?? 0,
            'compare_at_amount' => $template?->compare_at_amount,
            'currency' => $template?->currency ?? $this->store($product)->default_currency,
            'weight_g' => $template?->weight_g,
            'requires_shipping' => $template?->requires_shipping ?? true,
            'is_default' => false,
            'position' => $position,
            'status' => VariantStatus::Active,
        ]);

        InventoryItem::withoutGlobalScopes()->updateOrCreate(
            ['variant_id' => $variant->getKey()],
            [
                'store_id' => $product->store_id,
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ],
        );

        return $variant;
    }

    private function variantHasOrderLines(ProductVariant $variant): bool
    {
        return Schema::hasTable('order_lines')
            && DB::table('order_lines')->where('variant_id', $variant->getKey())->exists();
    }

    private function store(Product $product): Store
    {
        return Store::query()->findOrFail($product->store_id);
    }
}


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

namespace Database\Factories;

use App\Enums\InventoryPolicy;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\InventoryItem>
 */
class InventoryItemFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'variant_id' => fn (): int => ProductVariant::withoutEvents(
                fn (): int => ProductVariant::factory()->create()->getKey(),
            ),
            'store_id' => fn (array $attributes): mixed => ProductVariant::query()
                ->with('product')
                ->find($attributes['variant_id'])
                ?->product
                ?->store_id ?? Store::factory(),
            'quantity_on_hand' => fake()->numberBetween(0, 100),
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ];
    }

    public function outOfStock(): static
    {
        return $this->state(fn (array $attributes): array => [
            'quantity_on_hand' => 0,
            'quantity_reserved' => 0,
        ]);
    }

    public function continuePolicy(): static
    {
        return $this->state(fn (array $attributes): array => [
            'policy' => InventoryPolicy::Continue,
        ]);
    }

    public function lowStock(): static
    {
        return $this->state(fn (array $attributes): array => [
            'quantity_on_hand' => fake()->numberBetween(1, 3),
            'quantity_reserved' => 0,
        ]);
    }
}


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

namespace Database\Factories;

use App\Enums\ProductStatus;
use App\Models\InventoryItem;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(3, true));

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title),
            'status' => ProductStatus::Active,
            'description_html' => '<p>'.fake()->paragraph().'</p>',
            'vendor' => fake()->company(),
            'product_type' => fake()->randomElement(['Shirts', 'Pants', 'Shoes', 'Accessories', 'Electronics', 'Books']),
            'tags' => fake()->randomElements(['new', 'sale', 'trending', 'popular', 'limited'], fake()->numberBetween(1, 3)),
            'published_at' => now(),
        ];
    }

    public function draft(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ProductStatus::Draft,
            'published_at' => null,
        ]);
    }

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

    public function withVariants(int $count): static
    {
        return $this->afterCreating(function ($product) use ($count): void {
            ProductVariant::factory()
                ->count($count)
                ->for($product)
                ->create();
        });
    }

    public function withDefaultVariant(int $price = 2499): static
    {
        return $this->afterCreating(function ($product) use ($price): void {
            $variant = ProductVariant::factory()
                ->default()
                ->for($product)
                ->create([
                    'price_amount' => $price,
                    'currency' => $product->store->default_currency,
                ]);

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


message: The inventory event and factory guard are already partly in place in the worktree. I’m tightening the remaining enum casts/defaults and then I’ll update tests to assert the invariant directly instead of relying on duplicate manual inventory rows.


text: The inventory event and factory guard are already partly in place in the worktree. I’m tightening the remaining enum casts/defaults and then I’ll update tests to assert the invariant directly instead of relying on duplicate manual inventory rows.


cmd: sed -n '1,260p' tests/Feature/Catalog/CatalogFoundationTest.php


cmd: sed -n '1,260p' tests/Feature/Catalog/ProductServiceTest.php


cmd: sed -n '1,260p' tests/Feature/Catalog/InventoryServiceTest.php


cmd: sed -n '1,280p' tests/Feature/Catalog/VariantMatrixServiceTest.php


cmd: sed -n '1,220p' database/factories/ProductVariantFactory.php


command: sed -n '1,260p' tests/Feature/Catalog/CatalogFoundationTest.php

aggregated_output:
<?php

use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;

uses(RefreshDatabase::class);

test('catalog tables and seeded fixtures exist', function () {
    $this->seed(DatabaseSeeder::class);

    expect(Schema::hasColumns('products', ['store_id', 'title', 'handle', 'status', 'tags']))->toBeTrue()
        ->and(Schema::hasColumns('product_variants', ['product_id', 'sku', 'price_amount', 'requires_shipping']))->toBeTrue()
        ->and(Schema::hasColumns('inventory_items', ['store_id', 'variant_id', 'quantity_on_hand', 'quantity_reserved', 'policy']))->toBeTrue()
        ->and(Schema::hasColumns('collections', ['store_id', 'title', 'handle', 'type', 'status']))->toBeTrue()
        ->and(Product::withoutGlobalScopes()->count())->toBe(20)
        ->and(Collection::withoutGlobalScopes()->where('handle', 't-shirts')->exists())->toBeTrue()
        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->count())->toBe(3)
        ->and(Product::withoutGlobalScopes()->where('handle', 'sold-out-canvas-tote')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('deny')
        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-wool-scarf')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('continue');
});

test('catalog models are scoped to the resolved store', function () {
    $firstStore = Store::factory()->create();
    $secondStore = Store::factory()->create();

    Product::factory()->create(['store_id' => $firstStore->getKey(), 'handle' => 'first-store-product']);
    Product::factory()->create(['store_id' => $secondStore->getKey(), 'handle' => 'second-store-product']);

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

    expect(Product::query()->pluck('handle')->all())->toBe(['first-store-product']);
});


command: sed -n '1,260p' tests/Feature/Catalog/ProductServiceTest.php

aggregated_output:
<?php

use App\Enums\ProductStatus;
use App\Events\ProductStatusChanged;
use App\Exceptions\InvalidProductTransitionException;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\ProductService;
use App\Support\HandleGenerator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;

uses(RefreshDatabase::class);

test('handle generator creates unique handles per store', function () {
    $store = Store::factory()->create();

    Product::factory()->create([
        'store_id' => $store->getKey(),
        'title' => 'Classic Cotton T-Shirt',
        'handle' => 'classic-cotton-t-shirt',
    ]);

    $handle = app(HandleGenerator::class)->generate('Classic Cotton T-Shirt', 'products', $store->getKey());

    expect($handle)->toBe('classic-cotton-t-shirt-1');
});

test('product service creates products with a default variant and inventory', function () {
    $store = Store::factory()->create(['default_currency' => 'EUR']);
    app()->instance('current_store', $store);

    $product = app(ProductService::class)->create($store, [
        'title' => 'Merino Crew',
        'price_amount' => 4999,
        'tags' => ['new'],
    ]);

    expect($product->handle)->toBe('merino-crew')
        ->and($product->variants)->toHaveCount(1)
        ->and($product->variants->first()->price_amount)->toBe(4999)
        ->and($product->variants->first()->inventoryItem)->not->toBeNull();
});

test('product status transitions enforce activation preconditions and dispatch events', function () {
    Event::fake([ProductStatusChanged::class]);

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

    $product = Product::factory()->draft()->create([
        'store_id' => $store->getKey(),
        'title' => 'Draft Product',
    ]);

    expect(fn () => app(ProductService::class)->transitionStatus($product, ProductStatus::Active))
        ->toThrow(InvalidProductTransitionException::class);

    ProductVariant::factory()->default()->create([
        'product_id' => $product->getKey(),
        'price_amount' => 1200,
    ]);

    app(ProductService::class)->transitionStatus($product->refresh(), ProductStatus::Active);

    expect($product->refresh()->status)->toBe(ProductStatus::Active)
        ->and($product->published_at)->not->toBeNull();

    Event::assertDispatched(ProductStatusChanged::class);
});

test('duplicate non-empty skus are rejected within the same store', function () {
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    app(ProductService::class)->create($store, [
        'title' => 'First Product',
        'variants' => [
            ['sku' => 'DUP-1', 'price_amount' => 1000],
        ],
    ]);

    expect(fn () => app(ProductService::class)->create($store, [
        'title' => 'Second Product',
        'variants' => [
            ['sku' => 'DUP-1', 'price_amount' => 1200],
        ],
    ]))->toThrow(RuntimeException::class);
});


command: sed -n '1,260p' tests/Feature/Catalog/InventoryServiceTest.php

aggregated_output:
<?php

use App\Exceptions\InsufficientInventoryException;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\InventoryService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('inventory reserve release commit and restock update quantities transactionally', function () {
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    $product = Product::factory()->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::factory()->create(['product_id' => $product->getKey()]);
    $item = $variant->inventoryItem()->firstOrFail();
    $item->forceFill(['quantity_on_hand' => 10, 'quantity_reserved' => 0, 'policy' => 'deny'])->save();

    $service = app(InventoryService::class);

    $service->reserve($item, 4);
    expect($item->refresh()->quantity_reserved)->toBe(4);

    $service->release($item, 1);
    expect($item->refresh()->quantity_reserved)->toBe(3);

    $service->commit($item, 2);
    expect($item->refresh()->quantity_on_hand)->toBe(8)
        ->and($item->quantity_reserved)->toBe(1);

    $service->restock($item, 5);
    expect($item->refresh()->quantity_on_hand)->toBe(13);
});

test('deny policy blocks reservations above available stock while continue allows it', function () {
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    $denyItem = InventoryItem::factory()->create([
        'store_id' => $store->getKey(),
        'quantity_on_hand' => 1,
        'quantity_reserved' => 0,
        'policy' => 'deny',
    ]);

    expect(fn () => app(InventoryService::class)->reserve($denyItem, 2))
        ->toThrow(InsufficientInventoryException::class);

    $continueItem = InventoryItem::factory()->create([
        'store_id' => $store->getKey(),
        'quantity_on_hand' => 0,
        'quantity_reserved' => 0,
        'policy' => 'continue',
    ]);

    app(InventoryService::class)->reserve($continueItem, 2);

    expect($continueItem->refresh()->quantity_reserved)->toBe(2);
});


command: sed -n '1,280p' tests/Feature/Catalog/VariantMatrixServiceTest.php

aggregated_output:
<?php

use App\Models\Product;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\VariantMatrixService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('variant matrix rebuild creates missing cartesian combinations and inventory', function () {
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    $product = Product::factory()->create(['store_id' => $store->getKey()]);

    $size = ProductOption::factory()->create(['product_id' => $product->getKey(), 'name' => 'Size', 'position' => 0]);
    $small = ProductOptionValue::factory()->create(['product_option_id' => $size->getKey(), 'value' => 'S', 'position' => 0]);
    $medium = ProductOptionValue::factory()->create(['product_option_id' => $size->getKey(), 'value' => 'M', 'position' => 1]);

    $color = ProductOption::factory()->create(['product_id' => $product->getKey(), 'name' => 'Color', 'position' => 1]);
    $black = ProductOptionValue::factory()->create(['product_option_id' => $color->getKey(), 'value' => 'Black', 'position' => 0]);
    $white = ProductOptionValue::factory()->create(['product_option_id' => $color->getKey(), 'value' => 'White', 'position' => 1]);

    $variant = ProductVariant::factory()->default()->create([
        'product_id' => $product->getKey(),
        'price_amount' => 2500,
    ]);
    $variant->optionValues()->sync([$small->getKey(), $black->getKey()]);

    app(VariantMatrixService::class)->rebuildMatrix($product);

    expect($product->variants()->count())->toBe(4)
        ->and($product->variants()->whereHas('optionValues', fn ($query) => $query->whereKey($medium->getKey()))->count())->toBe(2)
        ->and($product->variants()->whereHas('optionValues', fn ($query) => $query->whereKey($white->getKey()))->count())->toBe(2)
        ->and($product->variants()->whereHas('inventoryItem')->count())->toBe(4);
});

test('variant matrix rebuild creates a default variant when there are no options', function () {
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    $product = Product::factory()->create(['store_id' => $store->getKey()]);
    $product->variants()->delete();

    app(VariantMatrixService::class)->rebuildMatrix($product);

    expect($product->variants()->count())->toBe(1)
        ->and($product->variants()->first()->is_default)->toBeTrue();
});


command: sed -n '1,220p' database/factories/ProductVariantFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Enums\VariantStatus;
use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ProductVariant>
 */
class ProductVariantFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'product_id' => Product::factory(),
            'sku' => 'SKU-'.fake()->unique()->numerify('####').'-'.fake()->lexify('???'),
            'barcode' => fake()->ean13(),
            'price_amount' => fake()->numberBetween(999, 19999),
            'compare_at_amount' => null,
            'currency' => 'EUR',
            'weight_g' => fake()->numberBetween(100, 5000),
            'requires_shipping' => true,
            'is_default' => false,
            'position' => 0,
            'status' => VariantStatus::Active,
        ];
    }

    public function onSale(): static
    {
        return $this->state(fn (array $attributes): array => [
            'compare_at_amount' => fake()->numberBetween(20000, 39999),
            'price_amount' => fake()->numberBetween(9999, 19999),
        ]);
    }

    public function digital(): static
    {
        return $this->state(fn (array $attributes): array => [
            'requires_shipping' => false,
            'weight_g' => 0,
        ]);
    }

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

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


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

use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;

uses(RefreshDatabase::class);

test('catalog tables and seeded fixtures exist', function () {
    $this->seed(DatabaseSeeder::class);

    expect(Schema::hasColumns('products', ['store_id', 'title', 'handle', 'status', 'tags']))->toBeTrue()
        ->and(Schema::hasColumns('product_variants', ['product_id', 'sku', 'price_amount', 'requires_shipping']))->toBeTrue()
        ->and(Schema::hasColumns('inventory_items', ['store_id', 'variant_id', 'quantity_on_hand', 'quantity_reserved', 'policy']))->toBeTrue()
        ->and(Schema::hasColumns('collections', ['store_id', 'title', 'handle', 'type', 'status']))->toBeTrue()
        ->and(Product::withoutGlobalScopes()->count())->toBe(20)
        ->and(Collection::withoutGlobalScopes()->where('handle', 't-shirts')->exists())->toBeTrue()
        ->and(Product::withoutGlobalScopes()->where('handle', 'classic-cotton-t-shirt')->first()?->variants()->count())->toBe(3)
        ->and(Product::withoutGlobalScopes()->where('handle', 'sold-out-canvas-tote')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('deny')
        ->and(Product::withoutGlobalScopes()->where('handle', 'backorder-wool-scarf')->first()?->variants()->first()?->inventoryItem?->policy?->value)->toBe('continue');
});

test('catalog models are scoped to the resolved store', function () {
    $firstStore = Store::factory()->create();
    $secondStore = Store::factory()->create();

    Product::factory()->create(['store_id' => $firstStore->getKey(), 'handle' => 'first-store-product']);
    Product::factory()->create(['store_id' => $secondStore->getKey(), 'handle' => 'second-store-product']);

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

    expect(Product::query()->pluck('handle')->all())->toBe(['first-store-product']);
});