output:
Chunk ID: 302b23
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2249
Output:
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
  author: laravel
---

# Laravel Best Practices

Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.

## Consistency First

Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.

Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.

## Quick Reference

### 1. Database Performance → `rules/db-performance.md`

- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates

### 2. Advanced Query Patterns → `rules/advanced-queries.md`

- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)

### 3. Security → `rules/security.md`

- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields

### 4. Caching → `rules/caching.md`

- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production

### 5. Eloquent Patterns → `rules/eloquent.md`

- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries

### 6. Validation & Forms → `rules/validation.md`

- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`

### 7. Configuration → `rules/config.md`

- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text

### 8. Testing Patterns → `rules/testing.md`

- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories

### 9. Queue & Job Patterns → `rules/queue-jobs.md`

- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios

### 10. Routing & Controllers → `rules/routing.md`

- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation

### 11. HTTP Client → `rules/http-client.md`

- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests

### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`

- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails

### 13. Error Handling → `rules/error-handling.md`

- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes

### 14. Task Scheduling → `rules/scheduling.md`

- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration

### 15. Architecture → `rules/architecture.md`

- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution

### 16. Migrations → `rules/migrations.md`

- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML

### 17. Collections → `rules/collections.md`

- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections

### 18. Blade & Views → `rules/blade-views.md`

- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props

### 19. Conventions & Style → `rules/style.md`

- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files

## How to Apply

Always use a sub-agent to read rule files and explore this skill's content.

1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version


output:
Chunk ID: 8137be
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1275
Output:
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
  author: laravel
---

# Pest Testing 4

## Documentation

Use `search-docs` for detailed Pest 4 patterns and documentation.

## Basic Usage

### Creating Tests

All tests must be written using Pest. Use `php artisan make:test --pest {name}`.

### Test Organization

- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.

### Basic Test Structure

Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.

<!-- Basic Pest Test Example -->
```php
it('is true', function () {
    expect(true)->toBeTrue();
});
```

### Running Tests

- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.

## Assertions

Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:

<!-- Pest Response Assertion -->
```php
it('returns all', function () {
    $this->postJson('/api/docs', [])->assertSuccessful();
});
```

| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |

## Mocking

Import mock function before use: `use function Pest\Laravel\mock;`

## Datasets

Use datasets for repetitive tests (validation rules, etc.):

<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
    expect($email)->not->toBeEmpty();
})->with([
    'james' => 'james@laravel.com',
    'taylor' => 'taylor@laravel.com',
]);
```

## Pest 4 Features

| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |

### Browser Test Example

Browser tests run in real browsers for full integration testing:

- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.

<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
    Notification::fake();

    $this->actingAs(User::factory()->create());

    $page = visit('/sign-in');

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

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

### Smoke Testing

Quickly validate multiple pages have no JavaScript errors:

<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);

$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```

### Visual Regression Testing

Capture and compare screenshots to detect visual changes.

### Test Sharding

Split tests across parallel processes for faster CI runs.

### Architecture Testing

Pest 4 includes architecture testing (from Pest 3):

<!-- Architecture Test Example -->
```php
arch('controllers')
    ->expect('App\Http\Controllers')
    ->toExtendNothing()
    ->toHaveSuffix('Controller');
```

## Common Pitfalls

- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests


server: laravel-boost

tool: application-info

type: text

database_engine: sqlite

roster_name: FORTIFY

package_name: laravel/fortify

roster_name: LARAVEL

package_name: laravel/framework

roster_name: PROMPTS

package_name: laravel/prompts

package_name: livewire/flux

roster_name: LIVEWIRE

package_name: livewire/livewire

roster_name: BOOST

package_name: laravel/boost

roster_name: MCP

package_name: laravel/mcp

roster_name: PAIL

package_name: laravel/pail

roster_name: PINT

package_name: laravel/pint

roster_name: SAIL

package_name: laravel/sail

roster_name: PEST

package_name: pestphp/pest

roster_name: PHPUNIT

package_name: phpunit/phpunit

roster_name: TAILWINDCSS

package_name: tailwindcss

models: App\Models\Refund App\Models\NavigationMenu App\Models\Discount App\Models\Product App\Models\Order App\Models\Theme App\Models\Fulfillment App\Models\TaxSettings App\Models\ShippingZone App\Models\Collection App\Models\Page App\Models\StoreSettings


tool: search-docs

packages: laravel/framework pestphp/pest

queries: rate limiter named limiter route middleware

text:
# 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]_"}]


message: 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.

phase: commentary


type: message

role: assistant

text: 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.


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


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


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


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


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


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

AdminPlatformApiTest.php

type: read

aggregated_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);
});

status: completed


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

aggregated_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);
});


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

aggregated_output:
<?php

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

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


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

aggregated_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());
            }
        });


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

aggregated_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));
    }
}


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

type: unknown

cmd: git status --short

aggregated_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: 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);
});