# Search Results

## Routing Rate Limiting Attaching Rate Limiters to Routes
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Attaching Rate Limiters to Routes

Rate limiters may be attached to routes or route groups using the `throttle` [middleware](/docs/{{version}}/middleware). The throttle middleware accepts the name of the rate limiter you wish to assign to the route:

```php
Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/audio', function () {
        // ...
    });

    Route::post('/video', function () {
        // ...
    });
});
```

---

## Validation Form Request Validation Authorizing Form Requests
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Authorizing Form Requests

The form request class also contains an `authorize` method. Within this method, you may determine if the authenticated user actually has the authority to update a given resource. For example, you may determine if a user actually owns a blog comment they are attempting to update. Most likely, you will interact with your [authorization gates and policies](/docs/{{version}}/authorization) within this method:

```php
use App\Models\Comment;

/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    $comment = Comment::find($this->route('comment'));

    return $comment && $this->user()->can('update', $comment);
}
```

Since all form requests extend the base Laravel request class, we may use the `user` method to access the currently authenticated user. Also, note the call to the `route` method in the example above. This method grants you access to the URI parameters defined on the route being called, such as the `{comment}` parameter in the example below:

```php
Route::post('/comment/{comment}');
```

Therefore, if your application is taking advantage of [route model binding](/docs/{{version}}/routing#route-model-binding), your code may be made even more succinct by accessing the resolved model as a property of the request:

```php
return $this->user()->can('update', $this->comment);
```

If the `authorize` method returns `false`, an HTTP response with a 403 status code will automatically be returned and your controller method will not execute.

If you plan to handle authorization logic for the request in another part of your application, you may remove the `authorize` method completely, or simply return `true`:

```php
/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    return true;
}
```

> [!NOTE]
> You may type-hint any dependencies you need within the `authorize` method's signature. They will automatically be resolved via the Laravel [service container](/docs/{{version}}/container).

---

## Http Tests Validation Assertions assertInvalid
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertInvalid

Assert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:

```php
$response->assertInvalid(['name', 'email']);
```

You may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:

```php
$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);
```

---

## Test Impact Analysis
Test Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md

---
title: Test Impact Analysis (TIA)
description: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.
---

# Test Impact Analysis (TIA)

Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.

A typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.

To get started, just add the `--tia` flag to any Pest invocation:

```bash
./vendor/bin/pest --parallel --tia
```

The first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.

Every subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:

```
Tests:    774 passed (2658 assertions, 7 affected, 767 replayed)
Duration: 0.74s
```

`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.

## How Pest Decides What To Run

For each file you've changed, Pest looks for tests that depend on it:

- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.
- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.
- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.
- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.
- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.
- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.

## Cosmetic Edits Don't Run Anything

Pest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.

The result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.

## Modes

Pest supports a few flags alongside `--tia`:

| Flag | Behaviour |
|---|---|
| `--tia` | Replay if a baseline graph exists, otherwise record. |
| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |
| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.

## Sharing The Baseline From CI

Recording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.

When Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.

Here's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:

```yaml
name: TIA Baseline
on:
  push: { branches: [main] }
  schedule: [{ cron: '0 3 * * *' }]
  workflow_dispatch:
jobs:
  baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.5', coverage: xdebug }
      - run: composer install --no-interaction --prefer-dist
      - run: ./vendor/bin/pest --parallel --tia --coverage
      - name: Stage baseline for upload
        shell: bash
        run: |
          mkdir -p .pest-tia-baseline
          cp -R "$HOME/.pest/tia"/*/. .pest-tia-baseline/
      - uses: actions/upload-artifact@v4
        with:
          name: pest-tia-baseline
          path: .pest-tia-baseline/
          retention-days: 30
```

After CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.

## Storage

Pest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.

Sharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.

## Custom Watch Patterns

If your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:

```php
pest()->tia()->watch([
    'config/billing/**/*.php' => ['tests/Feature/Billing'],

    //
]);
```

Each glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.

---

Now that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)

---

## Stress Testing The Stress function Requests Upload Data Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Upload Data Rate

Returns the request upload data rate in bytes per second.

```php
$result->requests()->upload()->data()->rate();
```

---

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

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

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

## Events

Laravel dispatches a variety of [events](/docs/{{version}}/events) during the authentication process. You may [define listeners](/docs/{{version}}/events) for any of the following events:

| Event Name                                     |
| ---------------------------------------------- |
| `Illuminate\Auth\Events\Registered`            |
| `Illuminate\Auth\Events\Attempting`            |
| `Illuminate\Auth\Events\Authenticated`         |
| `Illuminate\Auth\Events\Login`                 |
| `Illuminate\Auth\Events\Failed`                |
| `Illuminate\Auth\Events\Validated`             |
| `Illuminate\Auth\Events\Verified`              |
| `Illuminate\Auth\Events\Logout`                |
| `Illuminate\Auth\Events\CurrentDeviceLogout`   |
| `Illuminate\Auth\Events\OtherDeviceLogout`     |
| `Illuminate\Auth\Events\Lockout`               |
| `Illuminate\Auth\Events\PasswordReset`         |
| `Illuminate\Auth\Events\PasswordResetLinkSent` |

---

## Filtering Tests `only()`
Filtering Tests | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/filtering-tests.md

### `only()`

During development, you may want to focus on running specific tests while excluding all others. Pest provides two ways to do this: running only tests in a specific file, or running only a specific test within a file.

#### Running Only Tests in a File

When working on a specific feature, you can mark all tests in a file to run exclusively by calling the `pest()->only()` function at the top of your test file:

```php
<?php

pest()->only();

test('first test', function () {
    // This will run
});

test('second test', function () {
    // This will also run
});
```

All tests in files with `pest()->only()` will run, while tests in other files will be skipped.

#### Running a Single Test

To run only a specific test within a file, chain the `->only()` method to the test:

```php
test('sum', function () {
  $result = sum(1, 2);

  expect($result)->toBe(3);
})->only();

test('another test', function () {
  // This will be skipped
});
```

---

As your codebase grows, manually running your tests with filtering can become tedious. That's where skipping tests comes in. Skipping tests is a useful feature that allows developers to exclude specific tests from the test suite temporarily without deleting them entirely: [Skipping Tests →](/docs/skipping-tests)

---

## Stress Testing The Stress function Requests Download Data Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Download Data Rate

Returns the request download data rate in bytes per second.

```php
$result->requests()->download()->data()->rate();
```

---

## Telescope Installation Dashboard Authorization
Telescope | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/telescope.md

### Dashboard Authorization

The Telescope dashboard may be accessed via the `/telescope` route. By default, you will only be able to access this dashboard in the `local` environment. Within your `app/Providers/TelescopeServiceProvider.php` file, there is an [authorization gate](/docs/{{version}}/authorization#gates) definition. This authorization gate controls access to Telescope in **non-local** environments. You are free to modify this gate as needed to restrict access to your Telescope installation:

```php
use App\Models\User;

/**
 * Register the Telescope gate.
 *
 * This gate determines who can access Telescope in non-local environments.
 */
protected function gate(): void
{
    Gate::define('viewTelescope', function (User $user) {
        return in_array($user->email, [
            'taylor@laravel.com',
        ]);
    });
}
```

> [!WARNING]
> You should ensure you change your `APP_ENV` environment variable to `production` in your production environment. Otherwise, your Telescope installation will be publicly available.

---

## Routing Rate Limiting Defining Rate Limiters
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Defining Rate Limiters

Laravel includes powerful and customizable rate limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs.

Rate limiters may be defined within the `boot` method of your application's `App\Providers\AppServiceProvider` class:

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}
```

Rate limiters are defined using the `RateLimiter` facade's `for` method. The `for` method accepts a rate limiter name and a closure that returns the limit configuration that should apply to routes that are assigned to the rate limiter. Limit configuration are instances of the `Illuminate\Cache\RateLimiting\Limit` class. This class contains helpful "builder" methods so that you can quickly define your limit. The rate limiter name may be any string you wish:

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('global', function (Request $request) {
        return Limit::perMinute(1000);
    });
}
```

If the incoming request exceeds the specified rate limit, a response with a 429 HTTP status code will automatically be returned by Laravel. If you would like to define your own response that should be returned by a rate limit, you may use the `response` method:

```php
RateLimiter::for('global', function (Request $request) {
    return Limit::perMinute(1000)->response(function (Request $request, array $headers) {
        return response('Custom response...', 429, $headers);
    });
});
```

Since rate limiter callbacks receive the incoming HTTP request instance, you may build the appropriate rate limit dynamically based on the incoming request or authenticated user:

```php
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->vipCustomer()
        ? Limit::none()
        : Limit::perHour(10);
});
```

---

## Stress Testing The Stress function Requests Failed Rate
Stress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md

### Requests Failed Rate

Returns the number of requests that failed per second.

```php
$result->requests()->failed()->rate();
```

---

## Socialite Retrieving User Details Stateless Authentication
Socialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md

#### Stateless Authentication

The `stateless` method may be used to disable session state verification. This is useful when adding social authentication to a stateless API that does not utilize cookie based sessions:

```php
use Laravel\Socialite\Socialite;

return Socialite::driver('google')->stateless()->user();
```

---

## Arch Testing Modifiers `abstracts()`
Arch Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/arch-testing.md

### `abstracts()`

The `abstracts()` modifier allows you to restrict the expectation to only abstract classes.

```php
arch('app')
    ->expect('App')
    ->abstracts()
    ->toImplement(JsonSerializable::class);
```
---

In this section, you have learned how to perform architectural testing, ensuring that your application or library's architecture meets a specified set of architectural requirements. Next, have you ever wondered how to test the performance of your code? Let's explore [Stress Testing](/docs/stress-testing).

---

## Routing Rate Limiting Defining Rate Limiters Segmenting Rate Limits
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

#### Segmenting Rate Limits

Sometimes you may wish to segment rate limits by some arbitrary value. For example, you may wish to allow users to access a given route 100 times per minute per IP address. To accomplish this, you may use the `by` method when building your rate limit:

```php
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->vipCustomer()
        ? Limit::none()
        : Limit::perMinute(100)->by($request->ip());
});
```

To illustrate this feature using another example, we can limit access to the route to 100 times per minute per authenticated user ID or 10 times per minute per IP address for guests:

```php
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(100)->by($request->user()->id)
        : Limit::perMinute(10)->by($request->ip());
});
```

_[Truncated]_


Output:
Wall time: 3.3450 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Routing Rate Limiting Attaching Rate Limiters to Routes\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Attaching Rate Limiters to Routes\n\nRate limiters may be attached to routes or route groups using the `throttle` [middleware](/docs/{{version}}/middleware). The throttle middleware accepts the name of the rate limiter you wish to assign to the route:\n\n```php\nRoute::middleware(['throttle:uploads'])->group(function () {\n    Route::post('/audio', function () {\n        // ...\n    });\n\n    Route::post('/video', function () {\n        // ...\n    });\n});\n```\n\n---\n\n## Validation Form Request Validation Authorizing Form Requests\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Authorizing Form Requests\n\nThe form request class also contains an `authorize` method. Within this method, you may determine if the authenticated user actually has the authority to update a given resource. For example, you may determine if a user actually owns a blog comment they are attempting to update. Most likely, you will interact with your [authorization gates and policies](/docs/{{version}}/authorization) within this method:\n\n```php\nuse App\\Models\\Comment;\n\n/**\n * Determine if the user is authorized to make this request.\n */\npublic function authorize(): bool\n{\n    $comment = Comment::find($this->route('comment'));\n\n    return $comment && $this->user()->can('update', $comment);\n}\n```\n\nSince all form requests extend the base Laravel request class, we may use the `user` method to access the currently authenticated user. Also, note the call to the `route` method in the example above. This method grants you access to the URI parameters defined on the route being called, such as the `{comment}` parameter in the example below:\n\n```php\nRoute::post('/comment/{comment}');\n```\n\nTherefore, if your application is taking advantage of [route model binding](/docs/{{version}}/routing#route-model-binding), your code may be made even more succinct by accessing the resolved model as a property of the request:\n\n```php\nreturn $this->user()->can('update', $this->comment);\n```\n\nIf the `authorize` method returns `false`, an HTTP response with a 403 status code will automatically be returned and your controller method will not execute.\n\nIf you plan to handle authorization logic for the request in another part of your application, you may remove the `authorize` method completely, or simply return `true`:\n\n```php\n/**\n * Determine if the user is authorized to make this request.\n */\npublic function authorize(): bool\n{\n    return true;\n}\n```\n\n> [!NOTE]\n> You may type-hint any dependencies you need within the `authorize` method's signature. They will automatically be resolved via the Laravel [service container](/docs/{{version}}/container).\n\n---\n\n## Http Tests Validation Assertions assertInvalid\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertInvalid\n\nAssert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:\n\n```php\n$response->assertInvalid(['name', 'email']);\n```\n\nYou may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:\n\n```php\n$response->assertInvalid([\n    'name' => 'The name field is required.',\n    'email' => 'valid email address',\n]);\n```\n\n---\n\n## Test Impact Analysis\nTest Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md\n\n---\ntitle: Test Impact Analysis (TIA)\ndescription: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.\n---\n\n# Test Impact Analysis (TIA)\n\nTest Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.\n\nA typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.\n\nTo get started, just add the `--tia` flag to any Pest invocation:\n\n```bash\n./vendor/bin/pest --parallel --tia\n```\n\nThe first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.\n\nEvery subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:\n\n```\nTests:    774 passed (2658 assertions, 7 affected, 767 replayed)\nDuration: 0.74s\n```\n\n`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.\n\n## How Pest Decides What To Run\n\nFor each file you've changed, Pest looks for tests that depend on it:\n\n- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.\n- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.\n- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.\n- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.\n- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.\n- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.\n\n## Cosmetic Edits Don't Run Anything\n\nPest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.\n\nThe result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.\n\n## Modes\n\nPest supports a few flags alongside `--tia`:\n\n| Flag | Behaviour |\n|---|---|\n| `--tia` | Replay if a baseline graph exists, otherwise record. |\n| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |\n| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.\n\n## Sharing The Baseline From CI\n\nRecording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.\n\nWhen Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.\n\nHere's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:\n\n```yaml\nname: TIA Baseline\non:\n  push: { branches: [main] }\n  schedule: [{ cron: '0 3 * * *' }]\n  workflow_dispatch:\njobs:\n  baseline:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with: { fetch-depth: 0 }\n      - uses: shivammathur/setup-php@v2\n        with: { php-version: '8.5', coverage: xdebug }\n      - run: composer install --no-interaction --prefer-dist\n      - run: ./vendor/bin/pest --parallel --tia --coverage\n      - name: Stage baseline for upload\n        shell: bash\n        run: |\n          mkdir -p .pest-tia-baseline\n          cp -R \"$HOME/.pest/tia\"/*/. .pest-tia-baseline/\n      - uses: actions/upload-artifact@v4\n        with:\n          name: pest-tia-baseline\n          path: .pest-tia-baseline/\n          retention-days: 30\n```\n\nAfter CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.\n\n## Storage\n\nPest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.\n\nSharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.\n\n## Custom Watch Patterns\n\nIf your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:\n\n```php\npest()->tia()->watch([\n    'config/billing/**/*.php' => ['tests/Feature/Billing'],\n\n    //\n]);\n```\n\nEach glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.\n\n---\n\nNow that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)\n\n\n---\n\n## Stress Testing The Stress function Requests Upload Data Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Upload Data Rate\n\nReturns the request upload data rate in bytes per second.\n\n```php\n$result->requests()->upload()->data()->rate();\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Updated `DatabaseTokenRepository` Constructor Signature\n\n**Likelihood Of Impact: Very Low**\n\nThe constructor of the `Illuminate\\Auth\\Passwords\\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.\n\n---\n\n## Authentication Events\nAuthentication | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/authentication.md\n\n\n## Events\n\nLaravel dispatches a variety of [events](/docs/{{version}}/events) during the authentication process. You may [define listeners](/docs/{{version}}/events) for any of the following events:\n\n
\n\n| Event Name                                     |\n| ---------------------------------------------- |\n| `Illuminate\\Auth\\Events\\Registered`            |\n| `Illuminate\\Auth\\Events\\Attempting`            |\n| `Illuminate\\Auth\\Events\\Authenticated`         |\n| `Illuminate\\Auth\\Events\\Login`                 |\n| `Illuminate\\Auth\\Events\\Failed`                |\n| `Illuminate\\Auth\\Events\\Validated`             |\n| `Illuminate\\Auth\\Events\\Verified`              |\n| `Illuminate\\Auth\\Events\\Logout`                |\n| `Illuminate\\Auth\\Events\\CurrentDeviceLogout`   |\n| `Illuminate\\Auth\\Events\\OtherDeviceLogout`     |\n| `Illuminate\\Auth\\Events\\Lockout`               |\n| `Illuminate\\Auth\\Events\\PasswordReset`         |\n| `Illuminate\\Auth\\Events\\PasswordResetLinkSent` |\n\n
\n\n---\n\n## Filtering Tests `only()`\nFiltering Tests | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/filtering-tests.md\n\n\n### `only()`\n\nDuring development, you may want to focus on running specific tests while excluding all others. Pest provides two ways to do this: running only tests in a specific file, or running only a specific test within a file.\n\n#### Running Only Tests in a File\n\nWhen working on a specific feature, you can mark all tests in a file to run exclusively by calling the `pest()->only()` function at the top of your test file:\n\n```php\n<?php\n\npest()->only();\n\ntest('first test', function () {\n    // This will run\n});\n\ntest('second test', function () {\n    // This will also run\n});\n```\n\nAll tests in files with `pest()->only()` will run, while tests in other files will be skipped.\n\n#### Running a Single Test\n\nTo run only a specific test within a file, chain the `->only()` method to the test:\n\n```php\ntest('sum', function () {\n  $result = sum(1, 2);\n\n  expect($result)->toBe(3);\n})->only();\n\ntest('another test', function () {\n  // This will be skipped\n});\n```\n\n---\n\nAs your codebase grows, manually running your tests with filtering can become tedious. That's where skipping tests comes in. Skipping tests is a useful feature that allows developers to exclude specific tests from the test suite temporarily without deleting them entirely: [Skipping Tests →](/docs/skipping-tests)\n\n---\n\n## Stress Testing The Stress function Requests Download Data Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Download Data Rate\n\nReturns the request download data rate in bytes per second.\n\n```php\n$result->requests()->download()->data()->rate();\n```\n\n---\n\n## Telescope Installation Dashboard Authorization\nTelescope | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/telescope.md\n\n\n### Dashboard Authorization\n\nThe Telescope dashboard may be accessed via the `/telescope` route. By default, you will only be able to access this dashboard in the `local` environment. Within your `app/Providers/TelescopeServiceProvider.php` file, there is an [authorization gate](/docs/{{version}}/authorization#gates) definition. This authorization gate controls access to Telescope in **non-local** environments. You are free to modify this gate as needed to restrict access to your Telescope installation:\n\n```php\nuse App\\Models\\User;\n\n/**\n * Register the Telescope gate.\n *\n * This gate determines who can access Telescope in non-local environments.\n */\nprotected function gate(): void\n{\n    Gate::define('viewTelescope', function (User $user) {\n        return in_array($user->email, [\n            'taylor@laravel.com',\n        ]);\n    });\n}\n```\n\n> [!WARNING]\n> You should ensure you change your `APP_ENV` environment variable to `production` in your production environment. Otherwise, your Telescope installation will be publicly available.\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Defining Rate Limiters\n\nLaravel includes powerful and customizable rate limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs.\n\nRate limiters may be defined within the `boot` method of your application's `App\\Providers\\AppServiceProvider` class:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\n/**\n * Bootstrap any application services.\n */\npublic function boot(): void\n{\n    RateLimiter::for('api', function (Request $request) {\n        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());\n    });\n}\n```\n\nRate limiters are defined using the `RateLimiter` facade's `for` method. The `for` method accepts a rate limiter name and a closure that returns the limit configuration that should apply to routes that are assigned to the rate limiter. Limit configuration are instances of the `Illuminate\\Cache\\RateLimiting\\Limit` class. This class contains helpful \"builder\" methods so that you can quickly define your limit. The rate limiter name may be any string you wish:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\n/**\n * Bootstrap any application services.\n */\npublic function boot(): void\n{\n    RateLimiter::for('global', function (Request $request) {\n        return Limit::perMinute(1000);\n    });\n}\n```\n\nIf the incoming request exceeds the specified rate limit, a response with a 429 HTTP status code will automatically be returned by Laravel. If you would like to define your own response that should be returned by a rate limit, you may use the `response` method:\n\n```php\nRateLimiter::for('global', function (Request $request) {\n    return Limit::perMinute(1000)->response(function (Request $request, array $headers) {\n        return response('Custom response...', 429, $headers);\n    });\n});\n```\n\nSince rate limiter callbacks receive the incoming HTTP request instance, you may build the appropriate rate limit dynamically based on the incoming request or authenticated user:\n\n```php\nRateLimiter::for('uploads', function (Request $request) {\n    return $request->user()->vipCustomer()\n        ? Limit::none()\n        : Limit::perHour(10);\n});\n```\n\n---\n\n## Stress Testing The Stress function Requests Failed Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Failed Rate\n\nReturns the number of requests that failed per second.\n\n```php\n$result->requests()->failed()->rate();\n```\n\n---\n\n## Socialite Retrieving User Details Stateless Authentication\nSocialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md\n\n\n#### Stateless Authentication\n\nThe `stateless` method may be used to disable session state verification. This is useful when adding social authentication to a stateless API that does not utilize cookie based sessions:\n\n```php\nuse Laravel\\Socialite\\Socialite;\n\nreturn Socialite::driver('google')->stateless()->user();\n```\n\n---\n\n## Arch Testing Modifiers `abstracts()`\nArch Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/arch-testing.md\n\n\n### `abstracts()`\n\nThe `abstracts()` modifier allows you to restrict the expectation to only abstract classes.\n\n```php\narch('app')\n    ->expect('App')\n    ->abstracts()\n    ->toImplement(JsonSerializable::class);\n```\n---\n\nIn this section, you have learned how to perform architectural testing, ensuring that your application or library's architecture meets a specified set of architectural requirements. Next, have you ever wondered how to test the performance of your code? Let's explore [Stress Testing](/docs/stress-testing).\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters Segmenting Rate Limits\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n#### Segmenting Rate Limits\n\nSometimes you may wish to segment rate limits by some arbitrary value. For example, you may wish to allow users to access a given route 100 times per minute per IP address. To accomplish this, you may use the `by` method when building your rate limit:\n\n```php\nRateLimiter::for('uploads', function (Request $request) {\n    return $request->user()->vipCustomer()\n        ? Limit::none()\n        : Limit::perMinute(100)->by($request->ip());\n});\n```\n\nTo illustrate this feature using another example, we can limit access to the route to 100 times per minute per authenticated user ID or 10 times per minute per IP address for guests:\n\n```php\nRateLimiter::for('uploads', function (Request $request) {\n    return $request->user()\n        ? Limit::perMinute(100)->by($request->user()->id)\n        : Limit::perMinute(10)->by($request->ip());\n});\n```\n\n_[Truncated]_"}]


The docs confirm the route middleware should reference the named limiter via `throttle:api.admin`, and auth events are the right hook for login/logout/failed audit entries. I’m going to inspect the affected tests and current patches, then tighten the coverage around the QA findings.


sed -n '1,260p' tests/Feature/Api/AdminPlatformApiTest.php


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


sed -n '1,240p' tests/Feature/Foundation/AuditLoggingTest.php


sed -n '1,260p' app/Providers/AppServiceProvider.php


sed -n '1,220p' app/Services/AuditLogger.php


sed -n '1,260p' tests/Feature/Api/AdminPlatformApiTest.php

<?php

use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\PersonalAccessToken, plain_text: string}
 */
function adminPlatformApiToken(Store $store, array $abilities): array
{
    return adminApiToken($store, $abilities);
}

test('platform api creates organizations and stores for owner bearer tokens', function (): void {
    $store = adminPlatformApiStore();
    $user = adminPlatformApiUser();
    $token = adminApiBearerToken($store, ['manage-platform'], $user);

    $organizationResponse = $this->withToken($token)
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Platform API Org',
            'billing_email' => 'billing@platform-api.test',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Platform API Org')
        ->assertJsonPath('data.billing_email', 'billing@platform-api.test');

    $organizationId = $organizationResponse->json('data.id');

    $this->withToken($token)
        ->postJson('/api/admin/v1/platform/stores', [
            'organization_id' => $organizationId,
            'name' => 'Platform API Store',
            'handle' => 'platform-api-store',
            'default_currency' => 'EUR',
            'default_locale' => 'en',
            'timezone' => 'Europe/Berlin',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Platform API Store')
        ->assertJsonPath('data.handle', 'platform-api-store')
        ->assertJsonPath('data.status', 'active');

    $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();

    expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()
        ->and(DB::table('store_users')
            ->where('store_id', $store->getKey())
            ->where('user_id', $user->getKey())
            ->where('role', 'owner')
            ->exists())->toBeTrue();
});

test('platform api rejects non owner tokens and tokens without manage platform ability', function (): void {
    $store = adminPlatformApiStore();
    $staff = User::factory()->create([
        'email' => 'platform-staff@example.test',
    ]);
    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $staff->getKey(),
        'role' => 'staff',
        'created_at' => now(),
    ]);
    $readToken = adminPlatformApiToken($store, ['read-products']);
    $staffToken = adminApiBearerToken($store, ['manage-platform'], $staff);

    $this->withToken($staffToken)
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Forbidden Org',
            'billing_email' => 'forbidden@example.test',
        ])
        ->assertForbidden();

    $this->withToken($readToken['plain_text'])
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Forbidden Token Org',
            'billing_email' => 'forbidden-token@example.test',
        ])
        ->assertForbidden();
});

test('platform api accepts manage platform bearer tokens', function (): void {
    $store = adminPlatformApiStore();
    $token = adminPlatformApiToken($store, ['manage-platform']);

    $this->withToken($token['plain_text'])
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Token Platform Org',
            'billing_email' => 'token-platform@example.test',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Token Platform Org');

    expect($token['token']->refresh()->last_used_at)->not->toBeNull();
});

test('store me and invite endpoints expose membership and enforce role or ability', function (): void {
    $store = adminPlatformApiStore();
    $user = adminPlatformApiUser();
    $staff = User::factory()->create([
        'email' => 'store-staff@example.test',
    ]);
    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $staff->getKey(),
        'role' => 'staff',
        'created_at' => now(),
    ]);
    $readToken = adminPlatformApiToken($store, ['read-products']);
    $manageToken = adminPlatformApiToken($store, ['manage-platform']);
    $userToken = adminApiBearerToken($store, ['*'], $user);
    $staffToken = adminApiBearerToken($store, ['manage-platform'], $staff);

    $this->withToken($readToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertForbidden();

    $this->withToken($manageToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertCreated()
        ->assertJsonPath('data.email', 'new-staff@example.test')
        ->assertJsonPath('data.role', 'staff');

    $this->withToken($userToken)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
        ->assertOk()
        ->assertJsonPath('data.email', 'admin@acme.test')
        ->assertJsonPath('data.role', 'owner')
        ->assertJsonPath('data.permissions.1', 'read-products');

    $this->withToken($staffToken)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertForbidden();

    $this->withToken($manageToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'admin@acme.test',
            'role' => 'admin',
        ])
        ->assertStatus(409);
});


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

<?php

use App\Livewire\Admin\Dashboard as AdminDashboard;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminDashboardStore(): Store
{
    $store = Store::factory()->create();
    $user = adminDashboardUser();

    DB::table('store_users')->updateOrInsert(
        [
            'store_id' => $store->getKey(),
            'user_id' => $user->getKey(),
        ],
        [
            'role' => 'owner',
            'created_at' => now(),
        ],
    );

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

    return $store;
}

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

function adminDashboardOrder(Store $store, string $title, int $quantity, int $unitPrice, array $orderAttributes = []): Order
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create([
            'store_id' => $store->getKey(),
            'title' => $title,
        ]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();
    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'placed_at' => now(),
    ], $orderAttributes));

    OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => $title,
        'sku_snapshot' => 'DASH-'.str($title)->slug()->upper(),
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    return $order;
}

test('admin dashboard route requires authentication and renders store scoped metrics', function (): void {
    $store = adminDashboardStore();
    adminDashboardOrder($store, 'Dashboard Jacket', 2, 2500);
    adminDashboardOrder($store, 'Dashboard Cap', 1, 2000);

    $otherStore = Store::factory()->create();
    adminDashboardOrder($otherStore, 'Other Store Product', 1, 9900);

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

    $this->actingAs(adminDashboardUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin')
        ->assertSuccessful()
        ->assertSee('Dashboard')
        ->assertSee('70.00 EUR')
        ->assertSee('Dashboard Jacket')
        ->assertDontSee('Other Store Product');
});

test('admin dashboard recalculates kpis when date range changes', function (): void {
    $store = adminDashboardStore();
    $user = adminDashboardUser();

    adminDashboardOrder($store, 'Today Product', 1, 6000);
    adminDashboardOrder($store, 'Older Product', 1, 4000, [
        'placed_at' => now()->subDays(15),
    ]);

    Livewire::actingAs($user)
        ->test(AdminDashboard::class)
        ->assertSet('ordersCount', 2)
        ->assertSet('totalSales', 10000)
        ->set('dateRange', 'today')
        ->assertSet('ordersCount', 1)
        ->assertSet('totalSales', 6000)
        ->set('dateRange', 'custom')
        ->set('customStartDate', now()->subDays(20)->toDateString())
        ->set('customEndDate', now()->subDays(10)->toDateString())
        ->assertSet('ordersCount', 1)
        ->assertSet('totalSales', 4000);
});


sed -n '1,240p' tests/Feature/Foundation/AuditLoggingTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


sed -n '1,260p' app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Contracts\PaymentProvider;
use App\Events\CheckoutCompleted;
use App\Events\FulfillmentCreated;
use App\Events\FulfillmentDelivered;
use App\Events\FulfillmentShipped;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Events\OrderRefunded;
use App\Events\ProductCreated;
use App\Events\ProductDeleted;
use App\Events\ProductUpdated;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Http\Middleware\ResolveStore;
use App\Listeners\DispatchWebhooks;
use App\Models\Collection as ProductCollection;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\Fulfillment;
use App\Models\NavigationMenu;
use App\Models\Order;
use App\Models\Page;
use App\Models\PersonalAccessToken;
use App\Models\Product;
use App\Models\Refund;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\TaxSettings;
use App\Models\Theme;
use App\Models\User;
use App\Observers\AuditModelObserver;
use App\Observers\ProductObserver;
use App\Services\AuditLogger;
use App\Services\AnalyticsService;
use App\Services\NavigationService;
use App\Services\Payments\MockPaymentProvider;
use App\Services\SearchService;
use App\Services\ThemeSettingsService;
use App\Services\WebhookService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Events\Failed as AuthFailed;
use Illuminate\Auth\Events\Login as AuthLogin;
use Illuminate\Auth\Events\Logout as AuthLogout;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View as ViewInstance;
use Livewire\Livewire;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(PaymentProvider::class, MockPaymentProvider::class);

        Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
            return new CustomerUserProvider($app['hash'], $config['model']);
        });

        Auth::viaRequest('sanctum-compatible', function (Request $request): ?User {
            app()->forgetInstance('sanctum_personal_access_token');

            $plainTextToken = $request->bearerToken();

            if (! is_string($plainTextToken) || $plainTextToken === '') {
                return null;
            }

            $token = PersonalAccessToken::query()
                ->with('tokenable')
                ->where('token', hash('sha256', $this->plainTokenForHashing($plainTextToken)))
                ->first();

            if (! $token instanceof PersonalAccessToken || $token->isExpired() || ! $token->tokenable instanceof User) {
                return null;
            }

            $request->attributes->set('sanctum_personal_access_token', $token);
            app()->instance('sanctum_personal_access_token', $token);

            return $token->tokenable;
        });

        $this->app->singleton(ThemeSettingsService::class);
        $this->app->singleton(NavigationService::class);
        $this->app->singleton(SearchService::class);
        $this->app->singleton(AnalyticsService::class);
        $this->app->singleton(WebhookService::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
        $this->configureEventListeners();
        $this->configureLivewireMiddleware();
        $this->configureStorefrontViewData();
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );

        RateLimiter::for('api.storefront', function (Request $request): Limit {
            return Limit::perMinute(120)->by($request->ip());
        });

        RateLimiter::for('api.admin', function (Request $request): Limit {
            $token = $request->attributes->get('sanctum_personal_access_token');

            return Limit::perMinute(60)->by(match (true) {
                $token instanceof PersonalAccessToken => 'token:'.$token->getKey(),
                $request->user() instanceof User => 'user:'.$request->user()->getAuthIdentifier(),
                default => 'ip:'.$request->ip(),
            });
        });

        RateLimiter::for('checkout', function (Request $request): Limit {
            $sessionId = $request->hasSession() ? $request->session()->getId() : null;

            return Limit::perMinute(10)->by($sessionId ?: $request->ip());
        });

        RateLimiter::for('search', function (Request $request): Limit {
            return Limit::perMinute(30)->by($request->ip());
        });

        RateLimiter::for('analytics', function (Request $request): Limit {
            return Limit::perMinute(60)->by($request->ip());
        });

        RateLimiter::for('webhooks', function (Request $request): Limit {
            return Limit::perMinute(100)->by($request->ip());
        });

        Product::observe(ProductObserver::class);
        foreach ([
            Product::class,
            ProductCollection::class,
            Discount::class,
            Page::class,
            Theme::class,
            Order::class,
            Fulfillment::class,
            Refund::class,
            NavigationMenu::class,
            ShippingZone::class,
            StoreSettings::class,
            TaxSettings::class,
        ] as $model) {
            $model::observe(AuditModelObserver::class);
        }

        Authenticate::redirectUsing(function (Request $request): string {
            if ($request->is('admin*')) {
                return route('admin.login');
            }

            if ($request->is('account*')) {
                return route('account.login');
            }

            return route('login');
        });
    }

    protected function configureStorefrontViewData(): void
    {
        View::composer('layouts.storefront', function (ViewInstance $view): void {
            $store = app()->bound('current_store') ? app('current_store') : null;

            if (! $store instanceof Store) {
                $view->with([
                    'themeSettings' => [],
                    'mainNavigation' => [],
                    'footerNavigation' => [],
                ]);

                return;
            }

            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
            $navigation = app(NavigationService::class);

            $view->with([
                'themeSettings' => $themeSettings,
                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
            ]);
        });
    }

    protected function configureEventListeners(): void
    {
        Event::listen(AuthLogin::class, function (AuthLogin $event): void {
            $user = $event->user;

            if ($user instanceof User) {
                app(AuditLogger::class)->log('auth.login', userId: $user->getKey());
            }

            if ($user instanceof Customer) {
                app(AuditLogger::class)->log('customer.login', storeId: (int) $user->store_id, extra: [
                    'customer_id' => $user->getKey(),
                ]);
            }
        });

        Event::listen(AuthFailed::class, function (AuthFailed $event): void {
            $credentials = $event->credentials;
            $email = is_string($credentials['email'] ?? null) ? $credentials['email'] : null;

            app(AuditLogger::class)->log(
                event: $event->guard === 'customer' ? 'customer.failed_login' : 'auth.failed_login',
                extra: ['email' => $email],
            );
        });

        Event::listen(AuthLogout::class, function (AuthLogout $event): void {
            if ($event->user instanceof User) {
                app(AuditLogger::class)->log('auth.logout', userId: $event->user->getKey());
            }
        });


sed -n '1,220p' app/Services/AuditLogger.php

<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

class AuditLogger
{
    /**
     * @param  array<string, mixed>|null  $changes
     * @param  array<string, mixed>  $extra
     */
    public function log(
        string $event,
        ?int $userId = null,
        ?int $storeId = null,
        ?string $resourceType = null,
        ?int $resourceId = null,
        ?array $changes = null,
        array $extra = [],
    ): void {
        $request = request();
        $entry = array_filter([
            'timestamp' => now()->toIso8601String(),
            'event' => $event,
            'user_id' => $userId,
            'store_id' => $storeId,
            'resource_type' => $resourceType,
            'resource_id' => $resourceId,
            'ip' => $request->ip() ?? 'console',
            'user_agent' => (string) $request->userAgent(),
            'changes' => $changes,
            ...$extra,
        ], fn (mixed $value): bool => $value !== null);

        Log::channel('audit')->info((string) json_encode($entry, JSON_UNESCAPED_SLASHES));
    }
}


 M app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php
 M app/Http/Controllers/Api/Admin/V1/CollectionController.php
 M app/Http/Controllers/Api/Admin/V1/CustomerController.php
 M app/Http/Controllers/Api/Admin/V1/DiscountController.php
 M app/Http/Controllers/Api/Admin/V1/OrderController.php
 M app/Http/Controllers/Api/Admin/V1/OrderExportController.php
 M app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php
 M app/Http/Controllers/Api/Admin/V1/OrderRefundController.php
 M app/Http/Controllers/Api/Admin/V1/PageController.php
 M app/Http/Controllers/Api/Admin/V1/ProductController.php
 M app/Http/Controllers/Api/Admin/V1/SearchIndexController.php
 M app/Http/Controllers/Api/Admin/V1/ShippingRateController.php
 M app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php
 M app/Http/Controllers/Api/Admin/V1/StoreInviteController.php
 M app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
 M app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php
 M app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php
 M app/Http/Controllers/Api/Admin/V1/ThemeController.php
 M app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
 M app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
 M app/Http/Middleware/AuthenticateAdminApi.php
 M app/Http/Middleware/AuthenticatePlatformApi.php
 M app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
 M app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
 M app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php
 M app/Http/Requests/Api/Admin/V1/StoreProductRequest.php
 M app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php
 M app/Http/Resources/Storefront/V1/CheckoutResource.php
 M app/Listeners/DispatchWebhooks.php
 M app/Livewire/Admin/Collections/Form.php
 M app/Livewire/Admin/Collections/Index.php
 M app/Livewire/Admin/Customers/Show.php
 M app/Livewire/Admin/Dashboard.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Orders/Show.php
 M app/Livewire/Admin/Pages/Form.php
 M app/Livewire/Admin/Products/Form.php
 M app/Livewire/Admin/Products/Index.php
 M app/Livewire/Storefront/Cart/Show.php
 M app/Livewire/Storefront/CartDrawer.php
 M app/Livewire/Storefront/Checkout/Confirmation.php
 M app/Livewire/Storefront/Checkout/Show.php
 M app/Models/User.php
 M app/Observers/ProductObserver.php
 M app/Policies/FulfillmentPolicy.php
 M app/Providers/AppServiceProvider.php
 M app/Services/CheckoutService.php
 M app/Services/OrderService.php
 M app/Services/WebhookService.php
 M config/auth.php
 M config/logging.php
 M database/migrations/0001_01_01_000000_create_users_table.php
 M database/migrations/2026_05_04_180719_create_personal_access_tokens_table.php
 M database/seeders/UserSeeder.php
 M phpunit.xml
 M resources/views/livewire/admin/developers/index.blade.php
 M resources/views/livewire/storefront/checkout/confirmation.blade.php
 M routes/api.php
 M routes/web.php
 M specs/progress.md
 M tests/Browser/Storefront/AccessibilityTest.php
 M tests/Browser/Storefront/CheckoutTest.php
 M tests/Browser/Storefront/ResponsiveTest.php
 M tests/Feature/Admin/AppsDevelopersTest.php
 M tests/Feature/Admin/OrderManagementTest.php
 M tests/Feature/Api/AdminAnalyticsSummaryApiTest.php
 M tests/Feature/Api/AdminCatalogApiTest.php
 M tests/Feature/Api/AdminCollectionApiTest.php
 M tests/Feature/Api/AdminDiscountApiTest.php
 M tests/Feature/Api/AdminOrderApiTest.php
 M tests/Feature/Api/AdminOrderExportApiTest.php
 M tests/Feature/Api/AdminPageApiTest.php
 M tests/Feature/Api/AdminPlatformApiTest.php
 M tests/Feature/Api/AdminSearchIndexApiTest.php
 M tests/Feature/Api/AdminShippingSettingsApiTest.php
 M tests/Feature/Api/AdminStoreSettingsApiTest.php
 M tests/Feature/Api/AdminTaxSettingsApiTest.php
 M tests/Feature/Api/AdminThemeApiTest.php
 M tests/Feature/Api/StorefrontCheckoutApiTest.php
 M tests/Feature/Api/StorefrontOrderApiTest.php
 M tests/Feature/Checkout/CheckoutServiceTest.php
 M tests/Feature/Foundation/DatabaseConstraintTest.php
 M tests/Feature/Storefront/CartCheckoutUiTest.php
 M tests/Feature/Storefront/OrderViewsTest.php
 M tests/Feature/Webhooks/WebhookDeliveryTest.php
 M tests/Pest.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? .playwright-mcp/console-2026-05-04T18-29-51-216Z.log
?? .playwright-mcp/console-2026-05-04T19-40-17-533Z.log
?? .playwright-mcp/console-2026-05-04T19-41-54-656Z.log
?? .playwright-mcp/console-2026-05-04T19-42-38-927Z.log
?? .playwright-mcp/console-2026-05-04T19-43-19-460Z.log
?? .playwright-mcp/page-2026-05-04T19-40-17-792Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-32-155Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-38-803Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-45-838Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-03-888Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-11-333Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-18-576Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-33-049Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-46-285Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-54-905Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-06-284Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-13-354Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-24-514Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-39-120Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-45-593Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-55-690Z.yml
?? .playwright-mcp/page-2026-05-04T19-43-19-622Z.yml
?? app/Actions/SanitizeHtml.php
?? app/Events/CheckoutCompleted.php
?? app/Events/ProductCreated.php
?? app/Events/ProductDeleted.php
?? app/Events/ProductUpdated.php
?? app/Models/PersonalAccessToken.php
?? app/Observers/AuditModelObserver.php
?? app/Services/AuditLogger.php
?? app/Support/CheckoutAccessToken.php
?? tests/Feature/Foundation/AuditLoggingTest.php
?? tests/Feature/Security/


Output:
Chunk ID: 313a0d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1485
Output:
 M app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php
 M app/Http/Controllers/Api/Admin/V1/CollectionController.php
 M app/Http/Controllers/Api/Admin/V1/CustomerController.php
 M app/Http/Controllers/Api/Admin/V1/DiscountController.php
 M app/Http/Controllers/Api/Admin/V1/OrderController.php
 M app/Http/Controllers/Api/Admin/V1/OrderExportController.php
 M app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php
 M app/Http/Controllers/Api/Admin/V1/OrderRefundController.php
 M app/Http/Controllers/Api/Admin/V1/PageController.php
 M app/Http/Controllers/Api/Admin/V1/ProductController.php
 M app/Http/Controllers/Api/Admin/V1/SearchIndexController.php
 M app/Http/Controllers/Api/Admin/V1/ShippingRateController.php
 M app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php
 M app/Http/Controllers/Api/Admin/V1/StoreInviteController.php
 M app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
 M app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php
 M app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php
 M app/Http/Controllers/Api/Admin/V1/ThemeController.php
 M app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
 M app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
 M app/Http/Middleware/AuthenticateAdminApi.php
 M app/Http/Middleware/AuthenticatePlatformApi.php
 M app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
 M app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
 M app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php
 M app/Http/Requests/Api/Admin/V1/StoreProductRequest.php
 M app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php
 M app/Http/Resources/Storefront/V1/CheckoutResource.php
 M app/Listeners/DispatchWebhooks.php
 M app/Livewire/Admin/Collections/Form.php
 M app/Livewire/Admin/Collections/Index.php
 M app/Livewire/Admin/Customers/Show.php
 M app/Livewire/Admin/Dashboard.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Orders/Show.php
 M app/Livewire/Admin/Pages/Form.php
 M app/Livewire/Admin/Products/Form.php
 M app/Livewire/Admin/Products/Index.php
 M app/Livewire/Storefront/Cart/Show.php
 M app/Livewire/Storefront/CartDrawer.php
 M app/Livewire/Storefront/Checkout/Confirmation.php
 M app/Livewire/Storefront/Checkout/Show.php
 M app/Models/User.php
 M app/Observers/ProductObserver.php
 M app/Policies/FulfillmentPolicy.php
 M app/Providers/AppServiceProvider.php
 M app/Services/CheckoutService.php
 M app/Services/OrderService.php
 M app/Services/WebhookService.php
 M config/auth.php
 M config/logging.php
 M database/migrations/0001_01_01_000000_create_users_table.php
 M database/migrations/2026_05_04_180719_create_personal_access_tokens_table.php
 M database/seeders/UserSeeder.php
 M phpunit.xml
 M resources/views/livewire/admin/developers/index.blade.php
 M resources/views/livewire/storefront/checkout/confirmation.blade.php
 M routes/api.php
 M routes/web.php
 M specs/progress.md
 M tests/Browser/Storefront/AccessibilityTest.php
 M tests/Browser/Storefront/CheckoutTest.php
 M tests/Browser/Storefront/ResponsiveTest.php
 M tests/Feature/Admin/AppsDevelopersTest.php
 M tests/Feature/Admin/OrderManagementTest.php
 M tests/Feature/Api/AdminAnalyticsSummaryApiTest.php
 M tests/Feature/Api/AdminCatalogApiTest.php
 M tests/Feature/Api/AdminCollectionApiTest.php
 M tests/Feature/Api/AdminDiscountApiTest.php
 M tests/Feature/Api/AdminOrderApiTest.php
 M tests/Feature/Api/AdminOrderExportApiTest.php
 M tests/Feature/Api/AdminPageApiTest.php
 M tests/Feature/Api/AdminPlatformApiTest.php
 M tests/Feature/Api/AdminSearchIndexApiTest.php
 M tests/Feature/Api/AdminShippingSettingsApiTest.php
 M tests/Feature/Api/AdminStoreSettingsApiTest.php
 M tests/Feature/Api/AdminTaxSettingsApiTest.php
 M tests/Feature/Api/AdminThemeApiTest.php
 M tests/Feature/Api/StorefrontCheckoutApiTest.php
 M tests/Feature/Api/StorefrontOrderApiTest.php
 M tests/Feature/Checkout/CheckoutServiceTest.php
 M tests/Feature/Foundation/DatabaseConstraintTest.php
 M tests/Feature/Storefront/CartCheckoutUiTest.php
 M tests/Feature/Storefront/OrderViewsTest.php
 M tests/Feature/Webhooks/WebhookDeliveryTest.php
 M tests/Pest.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? .playwright-mcp/console-2026-05-04T18-29-51-216Z.log
?? .playwright-mcp/console-2026-05-04T19-40-17-533Z.log
?? .playwright-mcp/console-2026-05-04T19-41-54-656Z.log
?? .playwright-mcp/console-2026-05-04T19-42-38-927Z.log
?? .playwright-mcp/console-2026-05-04T19-43-19-460Z.log
?? .playwright-mcp/page-2026-05-04T19-40-17-792Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-32-155Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-38-803Z.yml
?? .playwright-mcp/page-2026-05-04T19-40-45-838Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-03-888Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-11-333Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-18-576Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-33-049Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-46-285Z.yml
?? .playwright-mcp/page-2026-05-04T19-41-54-905Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-06-284Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-13-354Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-24-514Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-39-120Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-45-593Z.yml
?? .playwright-mcp/page-2026-05-04T19-42-55-690Z.yml
?? .playwright-mcp/page-2026-05-04T19-43-19-622Z.yml
?? app/Actions/SanitizeHtml.php
?? app/Events/CheckoutCompleted.php
?? app/Events/ProductCreated.php
?? app/Events/ProductDeleted.php
?? app/Events/ProductUpdated.php
?? app/Models/PersonalAccessToken.php
?? app/Observers/AuditModelObserver.php
?? app/Services/AuditLogger.php
?? app/Support/CheckoutAccessToken.php
?? tests/Feature/Foundation/AuditLoggingTest.php
?? tests/Feature/Security/


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

use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\PersonalAccessToken, plain_text: string}
 */
function adminPlatformApiToken(Store $store, array $abilities): array
{
    return adminApiToken($store, $abilities);
}

test('platform api creates organizations and stores for owner bearer tokens', function (): void {
    $store = adminPlatformApiStore();
    $user = adminPlatformApiUser();
    $token = adminApiBearerToken($store, ['manage-platform'], $user);

    $organizationResponse = $this->withToken($token)
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Platform API Org',
            'billing_email' => 'billing@platform-api.test',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Platform API Org')
        ->assertJsonPath('data.billing_email', 'billing@platform-api.test');

    $organizationId = $organizationResponse->json('data.id');

    $this->withToken($token)
        ->postJson('/api/admin/v1/platform/stores', [
            'organization_id' => $organizationId,
            'name' => 'Platform API Store',
            'handle' => 'platform-api-store',
            'default_currency' => 'EUR',
            'default_locale' => 'en',
            'timezone' => 'Europe/Berlin',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Platform API Store')
        ->assertJsonPath('data.handle', 'platform-api-store')
        ->assertJsonPath('data.status', 'active');

    $store = Store::query()->where('handle', 'platform-api-store')->firstOrFail();

    expect(StoreSettings::query()->whereKey($store->getKey())->exists())->toBeTrue()
        ->and(DB::table('store_users')
            ->where('store_id', $store->getKey())
            ->where('user_id', $user->getKey())
            ->where('role', 'owner')
            ->exists())->toBeTrue();
});

test('platform api rejects non owner tokens and tokens without manage platform ability', function (): void {
    $store = adminPlatformApiStore();
    $staff = User::factory()->create([
        'email' => 'platform-staff@example.test',
    ]);
    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $staff->getKey(),
        'role' => 'staff',
        'created_at' => now(),
    ]);
    $readToken = adminPlatformApiToken($store, ['read-products']);
    $staffToken = adminApiBearerToken($store, ['manage-platform'], $staff);

    $this->withToken($staffToken)
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Forbidden Org',
            'billing_email' => 'forbidden@example.test',
        ])
        ->assertForbidden();

    $this->withToken($readToken['plain_text'])
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Forbidden Token Org',
            'billing_email' => 'forbidden-token@example.test',
        ])
        ->assertForbidden();
});

test('platform api accepts manage platform bearer tokens', function (): void {
    $store = adminPlatformApiStore();
    $token = adminPlatformApiToken($store, ['manage-platform']);

    $this->withToken($token['plain_text'])
        ->postJson('/api/admin/v1/platform/organizations', [
            'name' => 'Token Platform Org',
            'billing_email' => 'token-platform@example.test',
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Token Platform Org');

    expect($token['token']->refresh()->last_used_at)->not->toBeNull();
});

test('store me and invite endpoints expose membership and enforce role or ability', function (): void {
    $store = adminPlatformApiStore();
    $user = adminPlatformApiUser();
    $staff = User::factory()->create([
        'email' => 'store-staff@example.test',
    ]);
    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $staff->getKey(),
        'role' => 'staff',
        'created_at' => now(),
    ]);
    $readToken = adminPlatformApiToken($store, ['read-products']);
    $manageToken = adminPlatformApiToken($store, ['manage-platform']);
    $userToken = adminApiBearerToken($store, ['*'], $user);
    $staffToken = adminApiBearerToken($store, ['manage-platform'], $staff);

    $this->withToken($readToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertForbidden();

    $this->withToken($manageToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertCreated()
        ->assertJsonPath('data.email', 'new-staff@example.test')
        ->assertJsonPath('data.role', 'staff');

    $this->withToken($userToken)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/me")
        ->assertOk()
        ->assertJsonPath('data.email', 'admin@acme.test')
        ->assertJsonPath('data.role', 'owner')
        ->assertJsonPath('data.permissions.1', 'read-products');

    $this->withToken($staffToken)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'new-staff@example.test',
            'role' => 'staff',
        ])
        ->assertForbidden();

    $this->withToken($manageToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/invites", [
            'email' => 'admin@acme.test',
            'role' => 'admin',
        ])
        ->assertStatus(409);
});


Output:
Chunk ID: bae11a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 951
Output:
<?php

use App\Livewire\Admin\Dashboard as AdminDashboard;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminDashboardStore(): Store
{
    $store = Store::factory()->create();
    $user = adminDashboardUser();

    DB::table('store_users')->updateOrInsert(
        [
            'store_id' => $store->getKey(),
            'user_id' => $user->getKey(),
        ],
        [
            'role' => 'owner',
            'created_at' => now(),
        ],
    );

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

    return $store;
}

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

function adminDashboardOrder(Store $store, string $title, int $quantity, int $unitPrice, array $orderAttributes = []): Order
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create([
            'store_id' => $store->getKey(),
            'title' => $title,
        ]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();
    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'placed_at' => now(),
    ], $orderAttributes));

    OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => $title,
        'sku_snapshot' => 'DASH-'.str($title)->slug()->upper(),
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    return $order;
}

test('admin dashboard route requires authentication and renders store scoped metrics', function (): void {
    $store = adminDashboardStore();
    adminDashboardOrder($store, 'Dashboard Jacket', 2, 2500);
    adminDashboardOrder($store, 'Dashboard Cap', 1, 2000);

    $otherStore = Store::factory()->create();
    adminDashboardOrder($otherStore, 'Other Store Product', 1, 9900);

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

    $this->actingAs(adminDashboardUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin')
        ->assertSuccessful()
        ->assertSee('Dashboard')
        ->assertSee('70.00 EUR')
        ->assertSee('Dashboard Jacket')
        ->assertDontSee('Other Store Product');
});

test('admin dashboard recalculates kpis when date range changes', function (): void {
    $store = adminDashboardStore();
    $user = adminDashboardUser();

    adminDashboardOrder($store, 'Today Product', 1, 6000);
    adminDashboardOrder($store, 'Older Product', 1, 4000, [
        'placed_at' => now()->subDays(15),
    ]);

    Livewire::actingAs($user)
        ->test(AdminDashboard::class)
        ->assertSet('ordersCount', 2)
        ->assertSet('totalSales', 10000)
        ->set('dateRange', 'today')
        ->assertSet('ordersCount', 1)
        ->assertSet('totalSales', 6000)
        ->set('dateRange', 'custom')
        ->set('customStartDate', now()->subDays(20)->toDateString())
        ->set('customEndDate', now()->subDays(10)->toDateString())
        ->assertSet('ordersCount', 1)
        ->assertSet('totalSales', 4000);
});


Output:
Chunk ID: ab30b7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: a67945
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2230
Output:
<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Contracts\PaymentProvider;
use App\Events\CheckoutCompleted;
use App\Events\FulfillmentCreated;
use App\Events\FulfillmentDelivered;
use App\Events\FulfillmentShipped;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Events\OrderRefunded;
use App\Events\ProductCreated;
use App\Events\ProductDeleted;
use App\Events\ProductUpdated;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Http\Middleware\ResolveStore;
use App\Listeners\DispatchWebhooks;
use App\Models\Collection as ProductCollection;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\Fulfillment;
use App\Models\NavigationMenu;
use App\Models\Order;
use App\Models\Page;
use App\Models\PersonalAccessToken;
use App\Models\Product;
use App\Models\Refund;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\TaxSettings;
use App\Models\Theme;
use App\Models\User;
use App\Observers\AuditModelObserver;
use App\Observers\ProductObserver;
use App\Services\AuditLogger;
use App\Services\AnalyticsService;
use App\Services\NavigationService;
use App\Services\Payments\MockPaymentProvider;
use App\Services\SearchService;
use App\Services\ThemeSettingsService;
use App\Services\WebhookService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Events\Failed as AuthFailed;
use Illuminate\Auth\Events\Login as AuthLogin;
use Illuminate\Auth\Events\Logout as AuthLogout;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View as ViewInstance;
use Livewire\Livewire;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(PaymentProvider::class, MockPaymentProvider::class);

        Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
            return new CustomerUserProvider($app['hash'], $config['model']);
        });

        Auth::viaRequest('sanctum-compatible', function (Request $request): ?User {
            app()->forgetInstance('sanctum_personal_access_token');

            $plainTextToken = $request->bearerToken();

            if (! is_string($plainTextToken) || $plainTextToken === '') {
                return null;
            }

            $token = PersonalAccessToken::query()
                ->with('tokenable')
                ->where('token', hash('sha256', $this->plainTokenForHashing($plainTextToken)))
                ->first();

            if (! $token instanceof PersonalAccessToken || $token->isExpired() || ! $token->tokenable instanceof User) {
                return null;
            }

            $request->attributes->set('sanctum_personal_access_token', $token);
            app()->instance('sanctum_personal_access_token', $token);

            return $token->tokenable;
        });

        $this->app->singleton(ThemeSettingsService::class);
        $this->app->singleton(NavigationService::class);
        $this->app->singleton(SearchService::class);
        $this->app->singleton(AnalyticsService::class);
        $this->app->singleton(WebhookService::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
        $this->configureEventListeners();
        $this->configureLivewireMiddleware();
        $this->configureStorefrontViewData();
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );

        RateLimiter::for('api.storefront', function (Request $request): Limit {
            return Limit::perMinute(120)->by($request->ip());
        });

        RateLimiter::for('api.admin', function (Request $request): Limit {
            $token = $request->attributes->get('sanctum_personal_access_token');

            return Limit::perMinute(60)->by(match (true) {
                $token instanceof PersonalAccessToken => 'token:'.$token->getKey(),
                $request->user() instanceof User => 'user:'.$request->user()->getAuthIdentifier(),
                default => 'ip:'.$request->ip(),
            });
        });

        RateLimiter::for('checkout', function (Request $request): Limit {
            $sessionId = $request->hasSession() ? $request->session()->getId() : null;

            return Limit::perMinute(10)->by($sessionId ?: $request->ip());
        });

        RateLimiter::for('search', function (Request $request): Limit {
            return Limit::perMinute(30)->by($request->ip());
        });

        RateLimiter::for('analytics', function (Request $request): Limit {
            return Limit::perMinute(60)->by($request->ip());
        });

        RateLimiter::for('webhooks', function (Request $request): Limit {
            return Limit::perMinute(100)->by($request->ip());
        });

        Product::observe(ProductObserver::class);
        foreach ([
            Product::class,
            ProductCollection::class,
            Discount::class,
            Page::class,
            Theme::class,
            Order::class,
            Fulfillment::class,
            Refund::class,
            NavigationMenu::class,
            ShippingZone::class,
            StoreSettings::class,
            TaxSettings::class,
        ] as $model) {
            $model::observe(AuditModelObserver::class);
        }

        Authenticate::redirectUsing(function (Request $request): string {
            if ($request->is('admin*')) {
                return route('admin.login');
            }

            if ($request->is('account*')) {
                return route('account.login');
            }

            return route('login');
        });
    }

    protected function configureStorefrontViewData(): void
    {
        View::composer('layouts.storefront', function (ViewInstance $view): void {
            $store = app()->bound('current_store') ? app('current_store') : null;

            if (! $store instanceof Store) {
                $view->with([
                    'themeSettings' => [],
                    'mainNavigation' => [],
                    'footerNavigation' => [],
                ]);

                return;
            }

            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
            $navigation = app(NavigationService::class);

            $view->with([
                'themeSettings' => $themeSettings,
                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
            ]);
        });
    }

    protected function configureEventListeners(): void
    {
        Event::listen(AuthLogin::class, function (AuthLogin $event): void {
            $user = $event->user;

            if ($user instanceof User) {
                app(AuditLogger::class)->log('auth.login', userId: $user->getKey());
            }

            if ($user instanceof Customer) {
                app(AuditLogger::class)->log('customer.login', storeId: (int) $user->store_id, extra: [
                    'customer_id' => $user->getKey(),
                ]);
            }
        });

        Event::listen(AuthFailed::class, function (AuthFailed $event): void {
            $credentials = $event->credentials;
            $email = is_string($credentials['email'] ?? null) ? $credentials['email'] : null;

            app(AuditLogger::class)->log(
                event: $event->guard === 'customer' ? 'customer.failed_login' : 'auth.failed_login',
                extra: ['email' => $email],
            );
        });

        Event::listen(AuthLogout::class, function (AuthLogout $event): void {
            if ($event->user instanceof User) {
                app(AuditLogger::class)->log('auth.logout', userId: $event->user->getKey());
            }
        });


Output:
Chunk ID: 16f97a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 270
Output:
<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

class AuditLogger
{
    /**
     * @param  array<string, mixed>|null  $changes
     * @param  array<string, mixed>  $extra
     */
    public function log(
        string $event,
        ?int $userId = null,
        ?int $storeId = null,
        ?string $resourceType = null,
        ?int $resourceId = null,
        ?array $changes = null,
        array $extra = [],
    ): void {
        $request = request();
        $entry = array_filter([
            'timestamp' => now()->toIso8601String(),
            'event' => $event,
            'user_id' => $userId,
            'store_id' => $storeId,
            'resource_type' => $resourceType,
            'resource_id' => $resourceId,
            'ip' => $request->ip() ?? 'console',
            'user_agent' => (string) $request->userAgent(),
            'changes' => $changes,
            ...$extra,
        ], fn (mixed $value): bool => $value !== null);

        Log::channel('audit')->info((string) json_encode($entry, JSON_UNESCAPED_SLASHES));
    }
}


sed -n '260,520p' app/Providers/AppServiceProvider.php


sed -n '1,260p' app/Observers/AuditModelObserver.php


sed -n '1,220p' app/Http/Middleware/AuthenticatePlatformApi.php


rg -n "function adminApi(Token|BearerToken)|adminApiBearerToken|adminApiToken" tests app -g '*.php'


sed -n '260,520p' app/Providers/AppServiceProvider.php

        });

        foreach ([
            OrderCreated::class,
            OrderPaid::class,
            OrderCancelled::class,
            OrderRefunded::class,
            CheckoutCompleted::class,
            FulfillmentCreated::class,
            FulfillmentShipped::class,
            FulfillmentDelivered::class,
            ProductCreated::class,
            ProductUpdated::class,
            ProductDeleted::class,
        ] as $event) {
            Event::listen($event, DispatchWebhooks::class);
        }
    }

    protected function configureLivewireMiddleware(): void
    {
        Livewire::addPersistentMiddleware([
            EnsureUserEmailIsVerified::class,
            ResolveStore::class,
            CheckStoreRole::class,
        ]);
    }

    private function plainTokenForHashing(string $plainTextToken): string
    {
        if (str_contains($plainTextToken, '|')) {
            return (string) str($plainTextToken)->after('|');
        }

        return $plainTextToken;
    }
}


sed -n '1,260p' app/Observers/AuditModelObserver.php

<?php

namespace App\Observers;

use App\Models\Collection;
use App\Models\Discount;
use App\Models\Fulfillment;
use App\Models\NavigationMenu;
use App\Models\Order;
use App\Models\Page;
use App\Models\Product;
use App\Models\Refund;
use App\Models\ShippingZone;
use App\Models\StoreSettings;
use App\Models\TaxSettings;
use App\Models\Theme;
use App\Services\AuditLogger;
use Illuminate\Database\Eloquent\Model;

class AuditModelObserver
{
    /**
     * @var array<class-string<Model>, string>
     */
    private array $resourceTypes = [
        Product::class => 'product',
        Collection::class => 'collection',
        Discount::class => 'discount',
        Page::class => 'page',
        Theme::class => 'theme',
        Order::class => 'order',
        Fulfillment::class => 'fulfillment',
        Refund::class => 'refund',
        NavigationMenu::class => 'navigation_menu',
        ShippingZone::class => 'shipping_zone',
        StoreSettings::class => 'store_settings',
        TaxSettings::class => 'tax_setting',
    ];

    public function created(Model $model): void
    {
        $this->write($model, 'created');
    }

    public function updated(Model $model): void
    {
        $this->write($model, 'updated', $this->changes($model));
    }

    public function deleted(Model $model): void
    {
        $this->write($model, 'deleted');
    }

    public function restored(Model $model): void
    {
        $this->write($model, 'restored');
    }

    /**
     * @param  array<string, mixed>|null  $changes
     */
    private function write(Model $model, string $action, ?array $changes = null): void
    {
        $resourceType = $this->resourceTypes[$model::class] ?? null;

        if ($resourceType === null) {
            return;
        }

        app(AuditLogger::class)->log(
            event: $model instanceof StoreSettings && $action === 'updated'
                ? 'store.settings_changed'
                : "{$resourceType}.{$action}",
            userId: auth()->id(),
            storeId: $this->storeId($model),
            resourceType: $resourceType,
            resourceId: is_numeric($model->getKey()) ? (int) $model->getKey() : null,
            changes: $changes,
        );
    }

    /**
     * @return array<string, array{0: mixed, 1: mixed}>
     */
    private function changes(Model $model): array
    {
        return collect($model->getChanges())
            ->except(['updated_at'])
            ->mapWithKeys(fn (mixed $newValue, string $key): array => [
                $key => [$model->getOriginal($key), $newValue],
            ])
            ->all();
    }

    private function storeId(Model $model): ?int
    {
        $storeId = $model->getAttribute('store_id');

        if (is_numeric($storeId)) {
            return (int) $storeId;
        }

        if (app()->bound('current_store')) {
            $store = app('current_store');

            return $store instanceof \App\Models\Store ? $store->getKey() : null;
        }

        return null;
    }
}