output:
Chunk ID: fa1f20
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 87
Output:
M  app/Livewire/Storefront/Cart/Show.php
M  app/Livewire/Storefront/Checkout/Show.php
M  resources/views/livewire/storefront/cart/show.blade.php
M  specs/progress.md
M  tests/Feature/Storefront/CartCheckoutUiTest.php
[2026-05-04-codex-5-5-goal bcb373fb] Add cart estimates and discount handoff
 5 files changed, 325 insertions(+), 9 deletions(-)


message: Committed the cart estimate slice as `bcb373fb Add cart estimates and discount handoff`. The remaining roadmap now moves into Phase 5: orders, payments, refunds/fulfillment, and customer/admin order surfaces. I’m reading the Phase 5 specs and current schema gaps before choosing the next bounded commit.

phase: commentary


type: message

role: assistant

text: Committed the cart estimate slice as `bcb373fb Add cart estimates and discount handoff`. The remaining roadmap now moves into Phase 5: orders, payments, refunds/fulfillment, and customer/admin order surfaces. I’m reading the Phase 5 specs and current schema gaps before choosing the next bounded commit.


packages: laravel/framework pestphp/pest

queries: migrations foreign keys transactions model factories

queries: events queued listeners database transactions


cmd: sed -n '640,760p' specs/09-IMPLEMENTATION-ROADMAP.md


cmd: sed -n '1390,1465p' specs/09-IMPLEMENTATION-ROADMAP.md


cmd: rg -n "orders|order_lines|payments|refunds|fulfillments|OrderService|Payment|Mock|Phase 5|customer account" specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/05-BUSINESS-LOGIC.md specs/06-AUTH-AND-SECURITY.md specs/07-SEEDERS-AND-TEST-DATA.md specs/09-IMPLEMENTATION-ROADMAP.md


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Events Queued Event Listeners Queued Event Listeners and Database Transactions
Events | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/events.md

### Queued Event Listeners and Database Transactions

When queued listeners are dispatched within database transactions, they may be processed by the queue before the database transaction has committed. When this happens, any updates you have made to models or database records during the database transaction may not yet be reflected in the database. In addition, any models or database records created within the transaction may not exist in the database. If your listener depends on these models, unexpected errors can occur when the job that dispatches the queued listener is processed.

If your queue connection's `after_commit` configuration option is set to `false`, you may still indicate that a particular queued listener should be dispatched after all open database transactions have been committed by implementing the `ShouldQueueAfterCommit` interface on the listener class:

```php
<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    use InteractsWithQueue;
}
```

> [!NOTE]
> To learn more about working around these issues, please review the documentation regarding [queued jobs and database transactions](/docs/{{version}}/queues#jobs-and-database-transactions).

---

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

---

## Migrating From Phpunit Guide
Migrating From Phpunit Guide | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/migrating-from-phpunit-guide.md

---
title: Migrating from PHPUnit
description: Migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps.
---

# Migrating from PHPUnit

Pest is built on top of PHPUnit, so migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps. Once you have Pest installed, you should require the `pestphp/pest-plugin-drift` package as a "dev" dependency in your project.

```bash
composer require pestphp/pest-plugin-drift --dev
```

Drift is a simple yet powerful plugin that will automatically convert your PHPUnit tests to Pest, simply by running the `--drift` option.

```bash
./vendor/bin/pest --drift
```

So, typically, a PHPUnit test looks like this:

```php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function test_that_true_is_true(): void
    {
        $this->assertTrue(true);
    }
}
```

Should look like this after running `--drift`:

```php
test('true is true', function () {
    expect(true)->toBeTrue();
});
```

---
## Convert Phpunit tests only for certain folder

If you want to convert Phpunit tests only for certain folder, you can pass _path_ as first argument when calling `--drift`:

Example call, if you want to run the conversion for folder `/tests/Helpers`:
```console
/vendor/bin/pest --drift tests/Helpers
```

Output:
```console
/vendor/bin/pest --drift tests/Helpers

✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔

INFO  The [tests/Helpers] directory has been migrated to PEST with XY files changed.
```

---

The output will contain a summary of the conversion process, as well as a list of the files that were converted.

While most of your tests should be converted automatically, and you should be able to run them without any issues, there are some cases where you may need to manually convert some of your tests.

---

Of course, this particular chapter is only for those who are migrating from PHPUnit. Next, let's learn how you can contribute to the growth of Pest: [Community Guide](/docs/community-guide)

---

## 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();

    // ...
}
```

---

## Queues Dispatching Jobs Jobs & Database Transactions
Queues | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queues.md

### Jobs & Database Transactions

While it is perfectly fine to dispatch jobs within database transactions, you should take special care to ensure that your job will actually be able to execute successfully. When dispatching a job within a transaction, it is possible that the job will be processed by a worker before the parent transaction has committed. When this happens, any updates you have made to models or database records during the database transaction(s) may not yet be reflected in the database. In addition, any models or database records created within the transaction(s) may not exist in the database.

Thankfully, Laravel provides several methods of working around this problem. First, you may set the `after_commit` connection option in your queue connection's configuration array:

```php
'redis' => [
    'driver' => 'redis',
    // ...
    'after_commit' => true,
],
```

When the `after_commit` option is `true`, you may dispatch jobs within database transactions; however, Laravel will wait until the open parent database transactions have been committed before actually dispatching the job. Of course, if no database transactions are currently open, the job will be dispatched immediately.

If a transaction is rolled back due to an exception that occurs during the transaction, the jobs that were dispatched during that transaction will be discarded.

> [!NOTE]
> Setting the `after_commit` configuration option to `true` will also cause any queued event listeners, mailables, notifications, and broadcast events to be dispatched after all open database transactions have been committed.

---

## Pest V4 Is Here Now With Browser Testing
Pest V4 Is Here Now With Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest-v4-is-here-now-with-browser-testing.md

---
title: Pest v4 Is Here — Now with Browser Testing
description: Today, we're thrilled to announce Pest v4 — our biggest release yet, featuring powerful new browser testing with parallel support and full Laravel integration.
---

> To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: [Upgrade Guide →](/docs/upgrade-guide).

- [Browser Testing](#content-pest-v4-is-here-now-with-browser-testing)
- [Smoke Testing](#content-smoke-testing)
- [Visual Regression Testing](#content-visual-regression-testing)
- [Test Sharding](#content-test-sharding)
- [Type Coverage Is Much Faster](#content-type-coverage-is-much-faster)
- [Profanity Checking](#content-profanity-checking)
- [On Top of PHPUnit 12](#content-on-top-of-phpunit-12)

# Pest v4 Is Here — Now with Browser Testing

Today, we're thrilled to announce the release of **Pest v4**, bringing the biggest testing upgrade yet: powerful **[Browser Testing](/docs/browser-testing)**. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.

Here is the creator of Pest, [Nuno Maduro](https://twitter.com/enunomaduro), demoing the new browser testing features in Pest v4 at Laracon US:

[![Pest v4](/assets/pest4_play.png)](https://youtu.be/f5gAgwwwwOI?si=LtPpySZe3tf8qMjz&t=52)

Here is an example of Browser Testing using [Laravel](https://laravel.com):

```php
it('may reset the password', function () {
    // access any laravel testing helpers...
    Notification::fake();

    // access to the database — using the RefreshDatabase trait (even sqlite in memory...)
    $this->actingAs(User::factory()->create());

    $page = visit('/sign-in') // visit on a real browser...
        ->on()->mobile() // or ->desktop(), ->tablet(), etc...
        ->inDarkMode(); // or ->inLightMode()

    $page->assertSee('Sign In')
         ->click('Forgot Password?')
         ->type('email', 'nuno@laravel.com')
         ->press('Send Reset Link')
         ->assertSee('We have emailed your password reset link!')
         ->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()

    Notification::assertSent(ResetPassword::class);
});
```

With Pest v4's browser testing, you can:
- Seamlessly use **Laravel features** like `Event::fake()`, `assertAuthenticated()`, and model factories
- Use `RefreshDatabase`, even with SQLite in-memory databases, to ensure a clean state for each test
- Test on **multiple browsers** (Chrome, Firefox, Safari)
- Test on **different devices** and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)
- Switch **color schemes** (light/dark mode)
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)
- Run **parallel browser tests** for dramatically faster suites
- Take **screenshots** or pause tests for debugging
- …all with the elegance of Pest syntax
- **Playwright-based** — modern, fast, and reliable

To get started with browser testing in Pest, you need to install the Pest Browser Plugin:

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

npm install playwright@latest
npx playwright install
```

After, you may use the `visit()` function anywhere. Finally, running this test is as simple as executing `./vendor/bin/pest` in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.

## Smoke Testing

Smoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.

```php
$routes = ['/', '/about', '/contact'];

visit($routes)->assertNoSmoke();

// assertNoSmoke() is a shorthand for:
// - assertNoJavascriptErrors()
// - assertNoConsoleLogs()
```

## Visual Regression Testing

Want to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the `assertScreenshotMatches()` assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.

```php
$pages = visit(['/', '/about', '/contact']);

$pages->assertScreenshotMatches();
```

This is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the [Browser Testing documentation](/docs/browser-testing) for a complete guide on how to get started.

## Test Sharding

Pest v4 introduces **Test Sharding**, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.

This feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.

To get started with Test Sharding, you can use the `--shard` option when running Pest:

```bash
# GitHub Workflow One
./vendor/bin/pest --shard=1/4

# GitHub Workflow Two
./vendor/bin/pest --shard=2/4

# GitHub Workflow Three
./vendor/bin/pest --shard=3/4

# GitHub Workflow Four
./vendor/bin/pest --shard=4/4
```

You may combine this with the `--parallel` option to run your tests in parallel, and this way trully maximize your test suite execution speed:

```bash
./vendor/bin/pest --shard=1/4 --parallel
```

Finally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the `matrix` strategy to define multiple jobs that run different shards:

```yaml
strategy:
  matrix:
    shard: [1, 2, 3, 4]

name: Tests (Shard ${{ matrix.shard }}/4)

steps:
  - name: Run tests
    run: pest --parallel --shard ${{ matrix.shard }}/4
```

## Type Coverage Is Much Faster

Remember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.

Type coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.

In addition, Type Coverage now supports **Sharding**. This means you can run type coverage with the `--shard` option, just like you do with your tests.

## Profanity Checking

Pest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.

You can enable profanity checking by adding the `--profanity` option when running Pest:

```bash
./vendor/bin/pest --profanity
```

To start using Pest's Profanity plugin, you need to require the plugin via Composer.

```bash
composer require pestphp/pest-plugin-profanity --dev
```

After requiring the plugin, you may utilize the `--profanity` option to generate a report of your profanity.

```bash
./vendor/bin/pest --profanity
```

If any of your files contain profanity, they will be highlighted in red and displayed using their respective line
numbers and the profane word(s) that have been found.

As an example, `pr31(f*ck)` means that the word "fuck" was found on line 31.

To learn more about the Profanity plugin and how to configure it, check out the [Profanity documentation](/docs/profanity).

### Skip Locally or On CI

Pest v4 introduces the ability to conditionally skip tests based on the environment. You can use `skipLocally()` to skip tests when running locally, or `skipOnCi` to skip tests when running on a CI server.

```php
it('does not run locally', function () {
    // This test will be skipped when running locally
})->skipLocally();

it('does not run on CI', function () {
    // This test will be skipped when running on a CI server
})->skipOnCi();
```

### Miscellaneous Improvements

- You may now use `skipLocally()` or `skipOnCi` to conditionally skip tests based on the environment.
- The `not->toHaveSuspiciousCharacters()` arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on the `php` arch preset. This expectation requires the `intl` PHP extension.
- The expectation `toBeSlug` has been added to help you validate that a string is a valid slug.

### On Top of PHPUnit 12

Pest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the [PHPUnit 12 release announcement](https://phpunit.de/announcements/phpunit-12.html).

### Thanks To You, Pest v4 Is Here!

There's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our [installation guide](/docs/installation) for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our [upgrade guide](/docs/upgrade-guide).

Thank you for your continued support and feedback. We can't wait to see what you build with Pest v4!

---

Thank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: [Installation →](/docs/installation)

---

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

---

## Packages Resources Migrations
Packages | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/packages.md

### Migrations

If your package contains [database migrations](/docs/{{version}}/migrations), you may use the `publishesMigrations` method to inform Laravel that the given directory or file contains migrations. When Laravel publishes the migrations, it will automatically update the timestamp within their filename to reflect the current date and time:

```php
/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}
```

---

## Migrations Columns Available Column Types Relationship Types
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

#### Relationship Types

[foreignId](#column-method-foreignId)
[foreignIdFor](#column-method-foreignIdFor)
[foreignUlid](#column-method-foreignUlid)
[foreignUuid](#column-method-foreignUuid)
[morphs](#column-method-morphs)
[nullableMorphs](#column-method-nullableMorphs)

---

## Migrations Indexes Foreign Key Constraints Toggling Foreign Key Constraints
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

#### Toggling Foreign Key Constraints

You may enable or disable foreign key constraints within your migrations by using the following methods:

```php
Schema::enableForeignKeyConstraints();

Schema::disableForeignKeyConstraints();

Schema::withoutForeignKeyConstraints(function () {
    // Constraints disabled within this closure...
});
```

> [!WARNING]
> SQLite disables foreign key constraints by default. When using SQLite, make sure to [enable foreign key support](/docs/{{version}}/database#configuration) in your database configuration before attempting to create them in your migrations.

---

## Migrations Events
Migrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md

## Events

For convenience, each migration operation will dispatch an [event](/docs/{{version}}/events). All of the following events extend the base `Illuminate\Database\Events\MigrationEvent` class:

| Class                                            | Description                                      |
| ------------------------------------------------ | ------------------------------------------------ |
| `Illuminate\Database\Events\MigrationsStarted`   | A batch of migrations is about to be executed.   |
| `Illuminate\Database\Events\MigrationsEnded`     | A batch of migrations has finished executing.    |
| `Illuminate\Database\Events\MigrationStarted`    | A single migration is about to be executed.      |
| `Illuminate\Database\Events\MigrationEnded`      | A single migration has finished executing.       |
| `Illuminate\Database\Events\NoPendingMigrations` | A migration command found no pending migrations. |
| `Illuminate\Database\Events\SchemaDumped`        | A database schema dump has completed.            |
| `Illuminate\Database\Events\SchemaLoaded`        | An existing database schema dump has loaded.     |

---

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

## Events

When using the [Laravel application starter kits](/docs/{{version}}/starter-kits), Laravel dispatches an `Illuminate\Auth\Events\Verified` [event](/docs/{{version}}/events) during the email verification process. If you are manually handling email verification for your application, you may wish to manually dispatch these events after verification is completed.

---

## Telescope Available Watchers Event Watcher
Telescope | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/telescope.md

### Event Watcher

The event watcher records the payload, listeners, and broadcast data for any [events](/docs/{{version}}/events) dispatched by your application. The Laravel framework's internal events are ignored by the Event watcher.

---

## Events Queued Event Listeners
Events | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/events.md

## Queued Event Listeners

Queueing listeners can be beneficial if your listener is going to perform a slow task such as sending an email or making an HTTP request. Before using queued listeners, make sure to [configure your queue](/docs/{{version}}/queues) and start a queue worker on your server or local development environment.

To specify that a listener should be queued, add the `ShouldQueue` interface to the listener class. Listeners generated by the `make:listener` Artisan commands already have this interface imported into the current namespace so you can use it immediately:

```php
<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    // ...
}
```

That's it! Now, when an event handled by this listener is dispatched, the listener will automatically be queued by the event dispatcher using Laravel's [queue system](/docs/{{version}}/queues). If no exceptions are thrown when the listener is executed by the queue, the queued job will automatically be deleted after it has finished processing.

---

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

_[Truncated]_


output:
Wall time: 3.3513 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Events Queued Event Listeners Queued Event Listeners and Database Transactions\nEvents | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/events.md\n\n\n### Queued Event Listeners and Database Transactions\n\nWhen queued listeners are dispatched within database transactions, they may be processed by the queue before the database transaction has committed. When this happens, any updates you have made to models or database records during the database transaction may not yet be reflected in the database. In addition, any models or database records created within the transaction may not exist in the database. If your listener depends on these models, unexpected errors can occur when the job that dispatches the queued listener is processed.\n\nIf your queue connection's `after_commit` configuration option is set to `false`, you may still indicate that a particular queued listener should be dispatched after all open database transactions have been committed by implementing the `ShouldQueueAfterCommit` interface on the listener class:\n\n```php\n<?php\n\nnamespace App\\Listeners;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueueAfterCommit;\nuse Illuminate\\Queue\\InteractsWithQueue;\n\nclass SendShipmentNotification implements ShouldQueueAfterCommit\n{\n    use InteractsWithQueue;\n}\n```\n\n> [!NOTE]\n> To learn more about working around these issues, please review the documentation regarding [queued jobs and database transactions](/docs/{{version}}/queues#jobs-and-database-transactions).\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## Migrating From Phpunit Guide\nMigrating From Phpunit Guide | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/migrating-from-phpunit-guide.md\n\n---\ntitle: Migrating from PHPUnit\ndescription: Migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps.\n---\n\n# Migrating from PHPUnit\n\nPest is built on top of PHPUnit, so migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps. Once you have Pest installed, you should require the `pestphp/pest-plugin-drift` package as a \"dev\" dependency in your project.\n\n```bash\ncomposer require pestphp/pest-plugin-drift --dev\n```\n\nDrift is a simple yet powerful plugin that will automatically convert your PHPUnit tests to Pest, simply by running the `--drift` option.\n\n```bash\n./vendor/bin/pest --drift\n```\n\nSo, typically, a PHPUnit test looks like this:\n\n```php\n<?php\n\nnamespace Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    public function test_that_true_is_true(): void\n    {\n        $this->assertTrue(true);\n    }\n}\n```\n\nShould look like this after running `--drift`:\n\n```php\ntest('true is true', function () {\n    expect(true)->toBeTrue();\n});\n```\n\n---\n## Convert Phpunit tests only for certain folder\n\nIf you want to convert Phpunit tests only for certain folder, you can pass _path_ as first argument when calling `--drift`:\n\nExample call, if you want to run the conversion for folder `/tests/Helpers`: \n```console \n/vendor/bin/pest --drift tests/Helpers\n```\n\nOutput:\n```console\n/vendor/bin/pest --drift tests/Helpers\n\n✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔\n\nINFO  The [tests/Helpers] directory has been migrated to PEST with XY files changed.\n```\n\n---\n\nThe output will contain a summary of the conversion process, as well as a list of the files that were converted.\n\nWhile most of your tests should be converted automatically, and you should be able to run them without any issues, there are some cases where you may need to manually convert some of your tests.\n\n---\n\nOf course, this particular chapter is only for those who are migrating from PHPUnit. Next, let's learn how you can contribute to the growth of Pest: [Community Guide](/docs/community-guide)\n\n\n---\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## Queues Dispatching Jobs Jobs & Database Transactions\nQueues | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/queues.md\n\n\n### Jobs & Database Transactions\n\nWhile it is perfectly fine to dispatch jobs within database transactions, you should take special care to ensure that your job will actually be able to execute successfully. When dispatching a job within a transaction, it is possible that the job will be processed by a worker before the parent transaction has committed. When this happens, any updates you have made to models or database records during the database transaction(s) may not yet be reflected in the database. In addition, any models or database records created within the transaction(s) may not exist in the database.\n\nThankfully, Laravel provides several methods of working around this problem. First, you may set the `after_commit` connection option in your queue connection's configuration array:\n\n```php\n'redis' => [\n    'driver' => 'redis',\n    // ...\n    'after_commit' => true,\n],\n```\n\nWhen the `after_commit` option is `true`, you may dispatch jobs within database transactions; however, Laravel will wait until the open parent database transactions have been committed before actually dispatching the job. Of course, if no database transactions are currently open, the job will be dispatched immediately.\n\nIf a transaction is rolled back due to an exception that occurs during the transaction, the jobs that were dispatched during that transaction will be discarded.\n\n> [!NOTE]\n> Setting the `after_commit` configuration option to `true` will also cause any queued event listeners, mailables, notifications, and broadcast events to be dispatched after all open database transactions have been committed.\n\n---\n\n## Pest V4 Is Here Now With Browser Testing\nPest V4 Is Here Now With Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest-v4-is-here-now-with-browser-testing.md\n\n---\ntitle: Pest v4 Is Here — Now with Browser Testing\ndescription: Today, we're thrilled to announce Pest v4 — our biggest release yet, featuring powerful new browser testing with parallel support and full Laravel integration.\n---\n\n> To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: [Upgrade Guide →](/docs/upgrade-guide).\n\n- [Browser Testing](#content-pest-v4-is-here-now-with-browser-testing)\n- [Smoke Testing](#content-smoke-testing)\n- [Visual Regression Testing](#content-visual-regression-testing)\n- [Test Sharding](#content-test-sharding)\n- [Type Coverage Is Much Faster](#content-type-coverage-is-much-faster)\n- [Profanity Checking](#content-profanity-checking)\n- [On Top of PHPUnit 12](#content-on-top-of-phpunit-12)\n\n# Pest v4 Is Here — Now with Browser Testing\n\nToday, we're thrilled to announce the release of **Pest v4**, bringing the biggest testing upgrade yet: powerful **[Browser Testing](/docs/browser-testing)**. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.\n\nHere is the creator of Pest, [Nuno Maduro](https://twitter.com/enunomaduro), demoing the new browser testing features in Pest v4 at Laracon US:\n\n[![Pest v4](/assets/pest4_play.png)](https://youtu.be/f5gAgwwwwOI?si=LtPpySZe3tf8qMjz&t=52)\n\nHere is an example of Browser Testing using [Laravel](https://laravel.com):\n\n```php\nit('may reset the password', function () {\n    // access any laravel testing helpers...\n    Notification::fake();\n\n    // access to the database — using the RefreshDatabase trait (even sqlite in memory...)\n    $this->actingAs(User::factory()->create());\n\n    $page = visit('/sign-in') // visit on a real browser...\n        ->on()->mobile() // or ->desktop(), ->tablet(), etc...\n        ->inDarkMode(); // or ->inLightMode()\n\n    $page->assertSee('Sign In')\n         ->click('Forgot Password?')\n         ->type('email', 'nuno@laravel.com')\n         ->press('Send Reset Link')\n         ->assertSee('We have emailed your password reset link!')\n         ->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()\n\n    Notification::assertSent(ResetPassword::class);\n});\n```\n\nWith Pest v4's browser testing, you can:\n- Seamlessly use **Laravel features** like `Event::fake()`, `assertAuthenticated()`, and model factories\n- Use `RefreshDatabase`, even with SQLite in-memory databases, to ensure a clean state for each test\n- Test on **multiple browsers** (Chrome, Firefox, Safari)\n- Test on **different devices** and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)\n- Switch **color schemes** (light/dark mode)\n- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)\n- Run **parallel browser tests** for dramatically faster suites\n- Take **screenshots** or pause tests for debugging\n- …all with the elegance of Pest syntax\n- **Playwright-based** — modern, fast, and reliable\n\nTo get started with browser testing in Pest, you need to install the Pest Browser Plugin:\n\n```bash\ncomposer require pestphp/pest-plugin-browser --dev\n\nnpm install playwright@latest\nnpx playwright install\n```\n\nAfter, you may use the `visit()` function anywhere. Finally, running this test is as simple as executing `./vendor/bin/pest` in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.\n\n## Smoke Testing\n\nSmoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.\n\n```php\n$routes = ['/', '/about', '/contact'];\n\nvisit($routes)->assertNoSmoke();\n\n// assertNoSmoke() is a shorthand for:\n// - assertNoJavascriptErrors()\n// - assertNoConsoleLogs()\n```\n\n## Visual Regression Testing\n\nWant to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the `assertScreenshotMatches()` assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.\n\n```php\n$pages = visit(['/', '/about', '/contact']);\n\n$pages->assertScreenshotMatches();\n```\n\nThis is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the [Browser Testing documentation](/docs/browser-testing) for a complete guide on how to get started.\n\n## Test Sharding\n\nPest v4 introduces **Test Sharding**, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.\n\nThis feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.\n\nTo get started with Test Sharding, you can use the `--shard` option when running Pest:\n\n```bash\n# GitHub Workflow One\n./vendor/bin/pest --shard=1/4\n\n# GitHub Workflow Two\n./vendor/bin/pest --shard=2/4\n\n# GitHub Workflow Three\n./vendor/bin/pest --shard=3/4\n\n# GitHub Workflow Four\n./vendor/bin/pest --shard=4/4\n```\n\nYou may combine this with the `--parallel` option to run your tests in parallel, and this way trully maximize your test suite execution speed:\n\n```bash\n./vendor/bin/pest --shard=1/4 --parallel\n```\n\nFinally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the `matrix` strategy to define multiple jobs that run different shards:\n\n```yaml\nstrategy:\n  matrix:\n    shard: [1, 2, 3, 4]\n\nname: Tests (Shard ${{ matrix.shard }}/4)\n\nsteps:\n  - name: Run tests\n    run: pest --parallel --shard ${{ matrix.shard }}/4\n```\n\n## Type Coverage Is Much Faster\n\nRemember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.\n\nType coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.\n\nIn addition, Type Coverage now supports **Sharding**. This means you can run type coverage with the `--shard` option, just like you do with your tests.\n\n## Profanity Checking\n\nPest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.\n\nYou can enable profanity checking by adding the `--profanity` option when running Pest:\n\n```bash\n./vendor/bin/pest --profanity\n```\n\nTo start using Pest's Profanity plugin, you need to require the plugin via Composer.\n\n```bash\ncomposer require pestphp/pest-plugin-profanity --dev\n```\n\nAfter requiring the plugin, you may utilize the `--profanity` option to generate a report of your profanity.\n\n```bash\n./vendor/bin/pest --profanity\n```\n\n\n\nIf any of your files contain profanity, they will be highlighted in red and displayed using their respective line\nnumbers and the profane word(s) that have been found.\n\nAs an example, `pr31(f*ck)` means that the word \"fuck\" was found on line 31.\n\nTo learn more about the Profanity plugin and how to configure it, check out the [Profanity documentation](/docs/profanity).\n\n### Skip Locally or On CI\n\nPest v4 introduces the ability to conditionally skip tests based on the environment. You can use `skipLocally()` to skip tests when running locally, or `skipOnCi` to skip tests when running on a CI server.\n\n```php\nit('does not run locally', function () {\n    // This test will be skipped when running locally\n})->skipLocally();\n\nit('does not run on CI', function () {\n    // This test will be skipped when running on a CI server\n})->skipOnCi();\n```\n\n### Miscellaneous Improvements\n\n- You may now use `skipLocally()` or `skipOnCi` to conditionally skip tests based on the environment.\n- The `not->toHaveSuspiciousCharacters()` arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on the `php` arch preset. This expectation requires the `intl` PHP extension.\n- The expectation `toBeSlug` has been added to help you validate that a string is a valid slug.\n\n### On Top of PHPUnit 12\n\nPest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the [PHPUnit 12 release announcement](https://phpunit.de/announcements/phpunit-12.html).\n\n### Thanks To You, Pest v4 Is Here!\n\nThere's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our [installation guide](/docs/installation) for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our [upgrade guide](/docs/upgrade-guide).\n\nThank you for your continued support and feedback. We can't wait to see what you build with Pest v4!\n\n---\n\nThank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: [Installation →](/docs/installation)\n\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## Packages Resources Migrations\nPackages | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/packages.md\n\n\n### Migrations\n\nIf your package contains [database migrations](/docs/{{version}}/migrations), you may use the `publishesMigrations` method to inform Laravel that the given directory or file contains migrations. When Laravel publishes the migrations, it will automatically update the timestamp within their filename to reflect the current date and time:\n\n```php\n/**\n * Bootstrap any package services.\n */\npublic function boot(): void\n{\n    $this->publishesMigrations([\n        __DIR__.'/../database/migrations' => database_path('migrations'),\n    ]);\n}\n```\n\n---\n\n## Migrations Columns Available Column Types Relationship Types\nMigrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md\n\n\n#### Relationship Types\n\n
\n\n[foreignId](#column-method-foreignId)\n[foreignIdFor](#column-method-foreignIdFor)\n[foreignUlid](#column-method-foreignUlid)\n[foreignUuid](#column-method-foreignUuid)\n[morphs](#column-method-morphs)\n[nullableMorphs](#column-method-nullableMorphs)\n\n
\n\n---\n\n## Migrations Indexes Foreign Key Constraints Toggling Foreign Key Constraints\nMigrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md\n\n\n#### Toggling Foreign Key Constraints\n\nYou may enable or disable foreign key constraints within your migrations by using the following methods:\n\n```php\nSchema::enableForeignKeyConstraints();\n\nSchema::disableForeignKeyConstraints();\n\nSchema::withoutForeignKeyConstraints(function () {\n    // Constraints disabled within this closure...\n});\n```\n\n> [!WARNING]\n> SQLite disables foreign key constraints by default. When using SQLite, make sure to [enable foreign key support](/docs/{{version}}/database#configuration) in your database configuration before attempting to create them in your migrations.\n\n---\n\n## Migrations Events\nMigrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md\n\n\n## Events\n\nFor convenience, each migration operation will dispatch an [event](/docs/{{version}}/events). All of the following events extend the base `Illuminate\\Database\\Events\\MigrationEvent` class:\n\n
\n\n| Class                                            | Description                                      |\n| ------------------------------------------------ | ------------------------------------------------ |\n| `Illuminate\\Database\\Events\\MigrationsStarted`   | A batch of migrations is about to be executed.   |\n| `Illuminate\\Database\\Events\\MigrationsEnded`     | A batch of migrations has finished executing.    |\n| `Illuminate\\Database\\Events\\MigrationStarted`    | A single migration is about to be executed.      |\n| `Illuminate\\Database\\Events\\MigrationEnded`      | A single migration has finished executing.       |\n| `Illuminate\\Database\\Events\\NoPendingMigrations` | A migration command found no pending migrations. |\n| `Illuminate\\Database\\Events\\SchemaDumped`        | A database schema dump has completed.            |\n| `Illuminate\\Database\\Events\\SchemaLoaded`        | An existing database schema dump has loaded.     |\n\n
\n\n---\n\n## Verification Events\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n## Events\n\nWhen using the [Laravel application starter kits](/docs/{{version}}/starter-kits), Laravel dispatches an `Illuminate\\Auth\\Events\\Verified` [event](/docs/{{version}}/events) during the email verification process. If you are manually handling email verification for your application, you may wish to manually dispatch these events after verification is completed.\n\n---\n\n## Telescope Available Watchers Event Watcher\nTelescope | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/telescope.md\n\n\n### Event Watcher\n\nThe event watcher records the payload, listeners, and broadcast data for any [events](/docs/{{version}}/events) dispatched by your application. The Laravel framework's internal events are ignored by the Event watcher.\n\n---\n\n## Events Queued Event Listeners\nEvents | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/events.md\n\n\n## Queued Event Listeners\n\nQueueing listeners can be beneficial if your listener is going to perform a slow task such as sending an email or making an HTTP request. Before using queued listeners, make sure to [configure your queue](/docs/{{version}}/queues) and start a queue worker on your server or local development environment.\n\nTo specify that a listener should be queued, add the `ShouldQueue` interface to the listener class. Listeners generated by the `make:listener` Artisan commands already have this interface imported into the current namespace so you can use it immediately:\n\n```php\n<?php\n\nnamespace App\\Listeners;\n\nuse App\\Events\\OrderShipped;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\n\nclass SendShipmentNotification implements ShouldQueue\n{\n    // ...\n}\n```\n\nThat's it! Now, when an event handled by this listener is dispatched, the listener will automatically be queued by the event dispatcher using Laravel's [queue system](/docs/{{version}}/queues). If no exceptions are thrown when the listener is executed by the queue, the queued job will automatically be deleted after it has finished processing.\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_[Truncated]_"}]


command: sed -n '1390,1465p' specs/09-IMPLEMENTATION-ROADMAP.md

type: read

aggregated_output:
| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates an order from a completed checkout | complete checkout flow | order in DB with correct amounts, status=pending | happy path |
| generates sequential order numbers per store | create 3 orders in store A | order numbers are #1001, #1002, #1003 | happy path |
| creates order lines with snapshots | order from checkout with 2 items | order_lines have title_snapshot, sku_snapshot | happy path |
| commits inventory on order creation | variant on_hand=10, reserved=3; create order with qty 3 | on_hand=7, reserved=0 | happy path |
| marks cart as converted | create order from checkout | cart status = converted | happy path |
| dispatches OrderCreated event | use Event::fake(); create order | OrderCreated dispatched with order | happy path |
| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
| links order to customer when authenticated | checkout by logged-in customer | order.customer_id set | happy path |
| sets email from checkout on the order | guest checkout with email "test@example.com" | order.email = "test@example.com" | happy path |

---

#### tests/Feature/Orders/RefundTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a full refund | order with total 5000, payment captured; refund 5000 | refund in DB, financial_status = refunded | happy path |
| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
| rejects refund exceeding payment amount | payment amount 5000; attempt to refund 6000 | validation error | validation |
| restocks inventory when restock flag is true | refund with restock=true, qty 2 | inventory on_hand increased by 2 | happy path |
| does not restock when restock flag is false | refund with restock=false | inventory unchanged | edge case |
| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
| records refund reason | refund with reason "Customer requested" | refund.reason = "Customer requested" | happy path |

---

#### tests/Feature/Orders/FulfillmentTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a fulfillment for specific order lines | order with 2 lines (qty 3 and qty 2); fulfill line 1 qty 3 | fulfillment created, fulfillment_lines has 1 row | happy path |
| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
| updates order fulfillment status to fulfilled when all lines done | fulfill all remaining lines | order.fulfillment_status = fulfilled | happy path |
| adds tracking information | ship fulfillment with tracking_company="DHL", tracking_number="123456" | tracking info saved, shipped_at set | happy path |
| transitions fulfillment from pending to shipped | ship fulfillment | status = shipped | happy path |
| transitions fulfillment from shipped to delivered | deliver fulfillment | status = delivered | happy path |
| prevents fulfilling more than ordered quantity | order line qty 2; attempt to fulfill qty 3 | validation error | validation |
| fulfillment guard blocks fulfillment when financial_status is pending | order with financial_status=pending (bank transfer) | FulfillmentGuardException thrown | validation |
| fulfillment guard allows fulfillment when financial_status is paid | order with financial_status=paid | fulfillment created successfully | happy path |
| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |

---

#### tests/Feature/Payments/MockPaymentProviderTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
| generates mock reference ID | any charge | provider_payment_id starts with "mock_" | happy path |

---

#### tests/Feature/Payments/PaymentServiceTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| processes credit card payment and creates order as paid | checkout with credit card | order created with financial_status=paid, inventory committed | happy path |
| processes PayPal payment and creates order as paid | checkout with PayPal | order created with financial_status=paid, inventory committed | happy path |
| processes bank transfer and creates order as pending | checkout with bank transfer | order created with financial_status=pending, inventory reserved but not committed | happy path |
| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
| creates a payment record with correct method | charge with credit_card | payment row in DB with method=credit_card, provider=mock | happy path |

---

#### tests/Feature/Payments/BankTransferConfirmationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|

status: completed


command: sed -n '640,760p' specs/09-IMPLEMENTATION-ROADMAP.md

aggregated_output:
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
| `RefundStatus` | `Pending`, `Processed`, `Failed` |
| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |

#### Step 5.3: Payment Service (Mock PSP)

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |

**PaymentProvider interface methods:**

| Method | Description |
|--------|-------------|
| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |

**MockPaymentProvider requirements:**
- No external API calls, no webhooks, no redirects
- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
- Bank transfer: deferred capture, returns PaymentResult with status=pending
- Generates mock reference IDs (`mock_` + random string)

**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.

#### Step 5.3b: Admin Payment Confirmation

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |

**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
2. Update `financial_status` to `paid`, `status` to `paid`
3. Update payment record status to `captured`
4. Commit reserved inventory
5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
6. Dispatch `OrderPaid` event

#### Step 5.4: Order Service

**File to create:** `App\Services\OrderService`

| Method | Description |
|--------|-------------|
| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |

Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.

#### Step 5.5: Refund Service

**File to create:** `App\Services\RefundService`

| Method | Description |
|--------|-------------|
| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |

- Partial refunds: `financial_status` -> `partially_refunded`
- Full refunds: `financial_status` -> `refunded`
- Restock flag: calls `InventoryService::restock()` for each line

#### Step 5.6: Fulfillment Service

**File to create:** `App\Services\FulfillmentService`

| Method | Description |
|--------|-------------|
| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |

Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).

**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).

**Events to create:**

| Type | Name | Purpose |
|------|------|---------|
| Event | `OrderCreated` | Dispatched when order is created from checkout |
| Event | `OrderPaid` | Dispatched when payment is captured |
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |


output:
Chunk ID: 4b7011
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1458
Output:
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
| `RefundStatus` | `Pending`, `Processed`, `Failed` |
| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |

#### Step 5.3: Payment Service (Mock PSP)

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |

**PaymentProvider interface methods:**

| Method | Description |
|--------|-------------|
| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |

**MockPaymentProvider requirements:**
- No external API calls, no webhooks, no redirects
- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
- Bank transfer: deferred capture, returns PaymentResult with status=pending
- Generates mock reference IDs (`mock_` + random string)

**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.

#### Step 5.3b: Admin Payment Confirmation

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |

**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
2. Update `financial_status` to `paid`, `status` to `paid`
3. Update payment record status to `captured`
4. Commit reserved inventory
5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
6. Dispatch `OrderPaid` event

#### Step 5.4: Order Service

**File to create:** `App\Services\OrderService`

| Method | Description |
|--------|-------------|
| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |

Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.

#### Step 5.5: Refund Service

**File to create:** `App\Services\RefundService`

| Method | Description |
|--------|-------------|
| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |

- Partial refunds: `financial_status` -> `partially_refunded`
- Full refunds: `financial_status` -> `refunded`
- Restock flag: calls `InventoryService::restock()` for each line

#### Step 5.6: Fulfillment Service

**File to create:** `App\Services\FulfillmentService`

| Method | Description |
|--------|-------------|
| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |

Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).

**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).

**Events to create:**

| Type | Name | Purpose |
|------|------|---------|
| Event | `OrderCreated` | Dispatched when order is created from checkout |
| Event | `OrderPaid` | Dispatched when payment is captured |
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |


output:
Chunk ID: cb5afa
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1624
Output:
| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates an order from a completed checkout | complete checkout flow | order in DB with correct amounts, status=pending | happy path |
| generates sequential order numbers per store | create 3 orders in store A | order numbers are #1001, #1002, #1003 | happy path |
| creates order lines with snapshots | order from checkout with 2 items | order_lines have title_snapshot, sku_snapshot | happy path |
| commits inventory on order creation | variant on_hand=10, reserved=3; create order with qty 3 | on_hand=7, reserved=0 | happy path |
| marks cart as converted | create order from checkout | cart status = converted | happy path |
| dispatches OrderCreated event | use Event::fake(); create order | OrderCreated dispatched with order | happy path |
| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
| links order to customer when authenticated | checkout by logged-in customer | order.customer_id set | happy path |
| sets email from checkout on the order | guest checkout with email "test@example.com" | order.email = "test@example.com" | happy path |

---

#### tests/Feature/Orders/RefundTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a full refund | order with total 5000, payment captured; refund 5000 | refund in DB, financial_status = refunded | happy path |
| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
| rejects refund exceeding payment amount | payment amount 5000; attempt to refund 6000 | validation error | validation |
| restocks inventory when restock flag is true | refund with restock=true, qty 2 | inventory on_hand increased by 2 | happy path |
| does not restock when restock flag is false | refund with restock=false | inventory unchanged | edge case |
| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
| records refund reason | refund with reason "Customer requested" | refund.reason = "Customer requested" | happy path |

---

#### tests/Feature/Orders/FulfillmentTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a fulfillment for specific order lines | order with 2 lines (qty 3 and qty 2); fulfill line 1 qty 3 | fulfillment created, fulfillment_lines has 1 row | happy path |
| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
| updates order fulfillment status to fulfilled when all lines done | fulfill all remaining lines | order.fulfillment_status = fulfilled | happy path |
| adds tracking information | ship fulfillment with tracking_company="DHL", tracking_number="123456" | tracking info saved, shipped_at set | happy path |
| transitions fulfillment from pending to shipped | ship fulfillment | status = shipped | happy path |
| transitions fulfillment from shipped to delivered | deliver fulfillment | status = delivered | happy path |
| prevents fulfilling more than ordered quantity | order line qty 2; attempt to fulfill qty 3 | validation error | validation |
| fulfillment guard blocks fulfillment when financial_status is pending | order with financial_status=pending (bank transfer) | FulfillmentGuardException thrown | validation |
| fulfillment guard allows fulfillment when financial_status is paid | order with financial_status=paid | fulfillment created successfully | happy path |
| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |

---

#### tests/Feature/Payments/MockPaymentProviderTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
| generates mock reference ID | any charge | provider_payment_id starts with "mock_" | happy path |

---

#### tests/Feature/Payments/PaymentServiceTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| processes credit card payment and creates order as paid | checkout with credit card | order created with financial_status=paid, inventory committed | happy path |
| processes PayPal payment and creates order as paid | checkout with PayPal | order created with financial_status=paid, inventory committed | happy path |
| processes bank transfer and creates order as pending | checkout with bank transfer | order created with financial_status=pending, inventory reserved but not committed | happy path |
| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
| creates a payment record with correct method | charge with credit_card | payment row in DB with method=credit_card, provider=mock | happy path |

---

#### tests/Feature/Payments/BankTransferConfirmationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|


command:
/bin/zsh
-lc
git status --short

type: unknown

cmd: git status --short


command: rg -n "orders|order_lines|payments|refunds|fulfillments|OrderService|Payment|Mock|Phase 5|customer account" specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/05-BUSINESS-LOGIC.md specs/06-AUTH-AND-SECURITY.md specs/07-SEEDERS-AND-TEST-DATA.md specs/09-IMPLEMENTATION-ROADMAP.md

type: search

cmd: rg -n 'orders|order_lines|payments|refunds|fulfillments|OrderService|Payment|Mock|Phase 5|customer account' specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/05-BUSINESS-LOGIC.md specs/06-AUTH-AND-SECURITY.md specs/07-SEEDERS-AND-TEST-DATA.md specs/09-IMPLEMENTATION-ROADMAP.md

query: orders|order_lines|payments|refunds|fulfillments|OrderService|Payment|Mock|Phase 5|customer account

aggregated_output:
specs/06-AUTH-AND-SECURITY.md:248:| `read-orders` | List and view orders |
specs/06-AUTH-AND-SECURITY.md:249:| `write-orders` | Update orders, create fulfillments |
specs/06-AUTH-AND-SECURITY.md:311:| Staff | `'staff'` | Operational access. Can manage products, orders, collections, discounts, pages. Cannot access settings, themes, staff management, or developers. |
specs/06-AUTH-AND-SECURITY.md:312:| Support | `'support'` | Read-only access to products, orders, and customers. For customer service agents. |
specs/06-AUTH-AND-SECURITY.md:383:| OwnerAdminOrStaff | Owner, Admin, Staff | Create/update products, collections, discounts, pages, fulfillments, order updates |
specs/06-AUTH-AND-SECURITY.md:384:| AnyRole | Owner, Admin, Staff, Support | Read-only listing/viewing of products, orders, customers, collections, discounts |
specs/06-AUTH-AND-SECURITY.md:617:| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:744:| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:768:| Payment | `raw_json` | `encrypted:array` | Full payment gateway response (may contain sensitive card/account data) |
specs/06-AUTH-AND-SECURITY.md:935:### 7.1 Inbound Payment Webhooks
specs/06-AUTH-AND-SECURITY.md:937:> **Not applicable.** All payment processing happens in-process via the Mock PSP. There are no inbound payment webhooks from external providers (no Stripe, no PayPal). This section is preserved for reference only -- if a real PSP is integrated in the future, inbound webhook verification would follow the patterns described in the outbound webhook section below (HMAC signature verification).
specs/06-AUTH-AND-SECURITY.md:1108:| 14 | Webhook signature verification | HMAC-SHA256 for outbound platform webhooks (no inbound payment webhooks - Mock PSP is in-process) | Required |
specs/09-IMPLEMENTATION-ROADMAP.md:25:| Payments | Stripe + PayPal | Mock PSP (in-process) | No external API calls. MockPaymentProvider handles credit card, PayPal, bank transfer with magic card numbers for testing. |
specs/09-IMPLEMENTATION-ROADMAP.md:45:    P5[Phase 5: Payments, Orders, Fulfillment]
specs/09-IMPLEMENTATION-ROADMAP.md:176:Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:232:| Manage orders | Y | Y | Y | Read-only |
specs/09-IMPLEMENTATION-ROADMAP.md:233:| Process refunds | Y | Y | N | N |
specs/09-IMPLEMENTATION-ROADMAP.md:234:| Create fulfillments | Y | Y | Y | N |
specs/09-IMPLEMENTATION-ROADMAP.md:247:**Priority:** HIGH - storefront and orders depend on this.
specs/09-IMPLEMENTATION-ROADMAP.md:404:| `account/orders/` | `index.blade.php`, `show.blade.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:487:| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
specs/09-IMPLEMENTATION-ROADMAP.md:575:| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
specs/09-IMPLEMENTATION-ROADMAP.md:576:| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
specs/09-IMPLEMENTATION-ROADMAP.md:594:| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
specs/09-IMPLEMENTATION-ROADMAP.md:599:### Phase 5: Payments, Orders, Fulfillment
specs/09-IMPLEMENTATION-ROADMAP.md:607:3. `create_orders_table` - FK: `store_id`, `customer_id`
specs/09-IMPLEMENTATION-ROADMAP.md:608:4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
specs/09-IMPLEMENTATION-ROADMAP.md:609:5. `create_payments_table` - FK: `order_id`
specs/09-IMPLEMENTATION-ROADMAP.md:610:6. `create_refunds_table` - FK: `order_id`, `payment_id`
specs/09-IMPLEMENTATION-ROADMAP.md:611:7. `create_fulfillments_table` - FK: `order_id`
specs/09-IMPLEMENTATION-ROADMAP.md:624:| Model (with migration, factory, seeder) | `Payment` | Payment record |
specs/09-IMPLEMENTATION-ROADMAP.md:635:| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
specs/09-IMPLEMENTATION-ROADMAP.md:637:| `Payment` | `belongsTo(Order)` |
specs/09-IMPLEMENTATION-ROADMAP.md:638:| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
specs/09-IMPLEMENTATION-ROADMAP.md:649:| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
specs/09-IMPLEMENTATION-ROADMAP.md:650:| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
specs/09-IMPLEMENTATION-ROADMAP.md:654:#### Step 5.3: Payment Service (Mock PSP)
specs/09-IMPLEMENTATION-ROADMAP.md:660:| Enum | `App\Enum…34870 chars truncated…A.md:1632:    product_variants --> order_lines
specs/01-DATABASE-SCHEMA.md:1633:    orders --> payments
specs/01-DATABASE-SCHEMA.md:1634:    orders --> fulfillments
specs/01-DATABASE-SCHEMA.md:1635:    orders --> refunds
specs/01-DATABASE-SCHEMA.md:1636:    payments --> refunds
specs/01-DATABASE-SCHEMA.md:1637:    fulfillments --> fulfillment_lines
specs/01-DATABASE-SCHEMA.md:1638:    order_lines --> fulfillment_lines
specs/01-DATABASE-SCHEMA.md:1647:- **Batch 5** (depends on Batch 4): `product_option_values`, `inventory_items`, `cart_lines`, `checkouts`, `orders`, `analytics_events`, `analytics_daily`, `webhook_deliveries`
specs/01-DATABASE-SCHEMA.md:1648:- **Batch 6** (depends on Batch 5): `variant_option_values`, `order_lines`, `payments`, `fulfillments`
specs/01-DATABASE-SCHEMA.md:1649:- **Batch 7** (depends on Batch 6): `refunds`, `fulfillment_lines`
specs/01-DATABASE-SCHEMA.md:1663:5. **Date-based indexes for time series** - Tables with temporal queries (orders, analytics, search queries, webhook deliveries) include indexes on date/timestamp columns scoped by store for efficient range scans.
specs/01-DATABASE-SCHEMA.md:1678:| 6 - Payments, Orders, Fulfillment, Customers | customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines | 8 |
specs/03-ADMIN-UI.md:86:  Orders                         -> admin.orders.index
specs/03-ADMIN-UI.md:218:| `ordersCount` | `int` | Number of orders in range |
specs/03-ADMIN-UI.md:222:| `ordersChange` | `float` | Percentage change vs previous period |
specs/03-ADMIN-UI.md:225:| `ordersChartData` | `array` | Array of `{date, count}` for last 30 days |
specs/03-ADMIN-UI.md:432:| Body text | "This will archive X product(s). Products with orders cannot be permanently deleted." |
specs/03-ADMIN-UI.md:665:| Body text | "This product will be archived. Products with existing orders cannot be permanently removed." |
specs/03-ADMIN-UI.md:864:**Route:** `GET /admin/orders`
specs/03-ADMIN-UI.md:868:**Blade view:** `resources/views/livewire/admin/orders/index.blade.php`
specs/03-ADMIN-UI.md:887:| `orders` | Paginated query with search, status filter, sorting. Eager loads `customer`. |
specs/03-ADMIN-UI.md:899:| Order #  | Date           | Customer  | Payment   | Fulfill | Total  |
specs/03-ADMIN-UI.md:938:**Route:** `GET /admin/orders/{order}`
specs/03-ADMIN-UI.md:942:**Blade view:** `resources/views/livewire/admin/orders/show.blade.php`
specs/03-ADMIN-UI.md:948:| `order` | `Order` | The order model (eager loaded with lines, payments, fulfillments, customer) |
specs/03-ADMIN-UI.md:960:| `confirmPayment()` | Confirms bank transfer payment received (transitions financial_status to `paid`, commits inventory, dispatches `OrderPaid`) |
specs/03-ADMIN-UI.md:982:| o  Payment received - Jan 15, 2:31 PM       | 62701              |
specs/03-ADMIN-UI.md:1003:| [Confirm Payment] button                     |                    |
specs/03-ADMIN-UI.md:1017:- "Confirm payment" button (primary, shown only when `payment_method = bank_transfer` AND `financial_status = pending`). On click, calls `confirmPayment()` which transitions the order to `paid`, commits inventory, and dispatches `OrderPaid`.
specs/03-ADMIN-UI.md:1025:> **Cannot create fulfillment.** Payment must be confirmed before items can be fulfilled. Current financial status: *{financial_status}*.
specs/03-ADMIN-UI.md:1032:- Events include: Order placed, Payment received, Fulfillment created, Shipped, Delivered, Refunded (as applicable)
specs/03-ADMIN-UI.md:1049:#### Payment Details
specs/03-ADMIN-UI.md:1051:- Payment method as text (Credit Card / PayPal / Bank Transfer), derived from `order.payment_method` enum
specs/03-ADMIN-UI.md:1054:- Mock reference ID (provider_payment_id)
specs/03-ADMIN-UI.md:1055:- **"Confirm payment" button** (primary variant): Shown only when `payment_method = bank_transfer` AND `financial_status = pending`. Clicking calls `confirmPayment()`.
specs/03-ADMIN-UI.md:1065:  - "Mark as delivered" button (shown when fulfillment status is `shipped`). On click, calls `markAsDelivered(fulfillmentId)`. Sets `delivered_at` timestamp. When all fulfillments are delivered, the order's `fulfillment_status` transitions to `fulfilled`.
specs/03-ADMIN-UI.md:1152:| `customers` | Paginated, searched customers with `withCount('orders')` and `withSum('orders', 'total_amount')` |
specs/03-ADMIN-UI.md:1180:| `mount(Customer $customer)` | Loads customer with orders and addresses |
specs/03-ADMIN-UI.md:2140:| `ordersCount` | `int` | Orders count |
specs/03-ADMIN-UI.md:2173:| [line/bar chart - daily revenue/orders]                          |
specs/03-ADMIN-UI.md:2201:**Sales chart:** Larger chart than Dashboard, line or bar chart showing daily revenue/orders.
specs/03-ADMIN-UI.md:2410:| `App\Livewire\Admin\Orders\Index` | `livewire.admin.orders.index` | `GET /admin/orders` |
specs/03-ADMIN-UI.md:2411:| `App\Livewire\Admin\Orders\Show` | `livewire.admin.orders.show` | `GET /admin/orders/{order}` |