output:
Wall time: 3.5014 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Strings Fluent Strings `hash` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `hash` {.collection-method}\n\nThe `hash` method hashes the string using the given [algorithm](https://www.php.net/manual/en/function.hash-algos.php):\n\n```php\nuse Illuminate\\Support\\Str;\n\n$hashed = Str::of('secret')->hash(algorithm: 'sha256');\n\n// '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'\n```\n\n---\n\n## Pest3 Now Available New Configuration API\nPest3 Now Available | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest3-now-available.md\n\n\n## New Configuration API\n\nPest 1 / Pest 2's configuration API was a little bit confusing, the `uses()` function that was originally made only for having the `$this` variable within closure bound to the test case instance, ended up being used for pretty much everything.\n\nIn Pest 3, we've introduced a new configuration API that is more intuitive and easier to use. The new configuration API is based on the `pest()` function, which allows you to configure Pest using a fluent and expressive API.\n\n> Note: the `uses()` function is still available in Pest 3, and we don't have plans to remove it. However, we recommend using the new configuration API for new projects.\n\n```diff\n-uses(TestCase::class)->in(__DIR__);\n+pest()->extends(TestCase::class);\n\n-uses(TestCase::class, RefreshDatabase::class)->in('Features');\n+pest()->extends(TestCase::class)->use(RefreshDatabase::class)->in('Features');\n\n-uses()->compact();\n+pest()->printer()->compact();\n```\n\nAnd of course, any method that was available on the `uses()` API, like `->beforeEach()` or `->group()` is still available on the new `pest()` configuration API; we've just made it more intuitive and easier to use.\n\n---\n\n## Custom Helpers\nCustom Helpers | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/custom-helpers.md\n\n---\ntitle: Custom Helpers\ndescription: If you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.\n---\n\n# Custom Helpers\n\nIf you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.\n\nFor example, if your helper is specific to a certain test file, you may create the helper in the test file directly. Within your helper, you may invoke the `test()` function to access the test class instance that would normally be available via `$this`.\n\n```php\nuse App\\Models\\User;\nuse Tests\\TestCase;\n\nfunction asAdmin(): TestCase\n{\n    $user = User::factory()->create([\n        'admin' => true,\n    ]);\n\n    return test()->actingAs($user);\n}\n\nit('can manage users', function () {\n    asAdmin()->get('/users')->assertOk();\n})\n```\n\n> **Note:** If your helper creates a custom expectation, you should write a dedicated [custom expectation](/docs/custom-expectations) instead.\n\nIf your test helpers are utilized throughout your test suite, you may define them within the `tests/Pest.php` or `tests/Helpers.php` files. Alternatively, you can create a `tests/Helpers` directory to house your own helper files. All of these options will be automatically loaded by Pest.\n\n```php\nuse App\\Clients\\PaymentClient;\nuse Mockery;\n\n// tests/Pest.php or tests/Helpers.php\nfunction mockPayments(): object\n{\n    $client = Mockery::mock(PaymentClient::class);\n\n    //\n\n    return $client;\n}\n\n// tests/Feature/PaymentsTest.php\nit('may buy a book', function () {\n    $client = mockPayments();\n\n    //\n})\n```\n\nAs an alternative to defining helper methods as functions, you may define protected methods in your base test class and subsequently access them in your test cases using the `$this` variable.\n\n```php\nuse App\\Clients\\PaymentClient;\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\nuse Mockery;\n\n// tests/TestCase.php\nclass TestCase extends BaseTestCase\n{\n    protected function mockPayments(): void\n    {\n        $client = Mockery::mock(PaymentClient::class);\n\n        //\n\n        return $client;\n    }\n}\n\n// tests/Pest.php\npest()->extend(TestCase::class)->in('Feature');\n\n// tests/Feature/PaymentsTest.php\nit('may buy a book', function () {\n    $client = $this->mockPayments();\n\n    //\n})\n```\n\n---\n\nIn this section, we explored creating custom helpers. Digging deeper, you may even want to generate a custom expectation. Let's jump into that topic in the next chapter: [Custom Expectations](/docs/custom-expectations)\n\n\n---\n\n## Sanctum API Token Authentication Token Abilities Token Ability Middleware\nSanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md\n\n\n#### Token Ability Middleware\n\nSanctum also includes two middleware that may be used to verify that an incoming request is authenticated with a token that has been granted a given ability. To get started, define the following middleware aliases in your application's `bootstrap/app.php` file:\n\n```php\nuse Laravel\\Sanctum\\Http\\Middleware\\CheckAbilities;\nuse Laravel\\Sanctum\\Http\\Middleware\\CheckForAnyAbility;\n\n->withMiddleware(function (Middleware $middleware): void {\n    $middleware->alias([\n        'abilities' => CheckAbilities::class,\n        'ability' => CheckForAnyAbility::class,\n    ]);\n})\n```\n\nThe `abilities` middleware may be assigned to a route to verify that the incoming request's token has all of the listed abilities:\n\n```php\nRoute::get('/orders', function () {\n    // Token has both \"check-status\" and \"place-orders\" abilities...\n})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);\n```\n\nThe `ability` middleware may be assigned to a route to verify that the incoming request's token has *at least one* of the listed abilities:\n\n```php\nRoute::get('/orders', function () {\n    // Token has the \"check-status\" or \"place-orders\" ability...\n})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);\n```\n\n---\n\n## Hashing Basic Usage Hashing Passwords\nHashing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/hashing.md\n\n\n### Hashing Passwords\n\nYou may hash a password by calling the `make` method on the `Hash` facade:\n\n```php\n<?php\n\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Hash;\n\nclass PasswordController extends Controller\n{\n    /**\n     * Update the password for the user.\n     */\n    public function update(Request $request): RedirectResponse\n    {\n        // Validate the new password length...\n\n        $request->user()->fill([\n            'password' => Hash::make($request->newPassword)\n        ])->save();\n\n        return redirect('/profile');\n    }\n}\n```\n\n---\n\n## Pagination Basic Usage Customizing Pagination URLs Appending Hash Fragments\nPagination | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/pagination.md\n\n\n#### Appending Hash Fragments\n\nIf you need to append a \"hash fragment\" to URLs generated by the paginator, you may use the `fragment` method. For example, to append `#users` to the end of each pagination link, you should invoke the `fragment` method like so:\n\n```php\n$users = User::paginate(15)->fragment('users');\n```\n\n---\n\n## Cli Api Reference\nCli Api Reference | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/cli-api-reference.md\n\n---\ntitle: CLI API Reference\ndescription: In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.\n---\n\n# CLI API Reference\n\nIn the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.\n\n## Configuration\n\n- `--init`: Initialize a standard Pest configuration.\n- `--bootstrap <file>` A PHP script that is included before the tests run.\n- `-c|--configuration <file>`: Read configuration from XML file.\n- `--no-configuration`: Ignore default configuration file (phpunit.xml).\n- `--extension <class>`: Register test runner extension with bootstrap <class>.\n- `--no-extensions`: Do not load PHPUnit extensions.\n- `--include-path <path(s)>`: Prepend PHP's include_path with given path(s).\n- `-d <key[=value]>`: Set a php.ini value.\n- `--cache-directory <dir>`: Specify cache directory.\n- `--generate-configuration`: Generate configuration file with suggested settings.\n- `--migrate-configuration`: Migrate configuration file to current format.\n- `--generate-baseline <file>`: Generate baseline for issues.\n- `--use-baseline <file>`: Use baseline to ignore issues.\n- `--ignore-baseline`: Do not use baseline to ignore issues.\n- `--test-directory`: Specify test directory containing Pest.php, TestCase.php, helpers and your tests. Default: tests\n\n## Selection\n\n- `--bail`: Stop execution upon first not-passed test.\n- `--ci`: Ignore focused tests using `->only()` and run the entire test suite.\n- `--todos`: Output to standard output the list of todos.\n- `--notes`: Output to standard output tests with notes.\n- `--issue`: Output to standard output tests with the given issue number.\n- `--pr`: Output to standard output tests with the given pull request number.\n- `--pull-request`: Output to standard output tests with the given pull request number (alias for `--pr`).\n- `--flaky`: Output to standard output tests marked as flaky.\n- `--retry`: Run non-passing tests first and stop execution upon first error or failure.\n- `--list-suites` List available test suites.\n- `--testsuite <name>`: Only run tests from the specified test suite(s).\n- `--exclude-testsuite <name>`: Exclude tests from the specified test suite(s).\n- `--list-groups`: List available test groups.\n- `--group <name>`: Only run tests from the specified group(s).\n- `--exclude-group <name>`: Exclude tests from the specified group(s).\n- `--covers <name>`: Only run tests that intend to cover `<name>`.\n- `--uses <name>`: Only run tests that intend to use `<name>`.\n- `--requires-php-extension <name>`: Only run tests that require PHP extension <name>.\n- `--list-test-files`: List available test files.\n- `--list-tests`: List available tests.\n- `--list-tests-xml <file>`: List available tests in XML format.\n- `--filter <pattern>`: Filter which tests to run\n- `--exclude-filter <pattern>`: Exclude tests for the specified filter pattern.\n- `--test-suffix <suffixes>`: Only search for test in files with specified suffix(es). Default: Test.php,.phpt\n\n## Execution\n\n- `--parallel` Run tests in parallel.\n- `--shard <index/total>`: Run only the given shard of tests (e.g. `--shard=1/5`). Uses time-balanced distribution when `tests/.pest/shards.json` exists.\n- `--update-shards`: Update `tests/.pest/shards.json` with test timing data for time-balanced sharding. Can be combined with `--parallel`.\n- `--update-snapshots`: Update snapshots for tests using the \"toMatchSnapshot\" expectation.\n- `--globals-backup`: Backup and restore $GLOBALS for each test.\n- `--static-backup`: Backup and restore static properties for each test.\n- `--strict-coverage`: Be strict about code coverage metadata.\n- `--strict-global-state`: Be strict about changes to global state.\n- `--disallow-test-output`: Be strict about output during tests.\n- `--enforce-time-limit`: Enforce time limit based on test size.\n- `--default-time-limit <sec>`: Timeout in seconds for tests that have no declared size.\n- `--dont-report-useless-tests`: Do not report tests that do not test anything.\n- `--stop-on-defect`: Stop execution upon first not-passed test.\n- `--stop-on-error`: Stop execution upon first error.\n- `--stop-on-failure`: Stop execution upon first error or failure.\n- `--stop-on-warning`: Stop execution upon first warning.\n- `--stop-on-risky`: Stop execution upon first risky test.\n- `--stop-on-deprecation`: Stop after first test that triggered a deprecation.\n- `--stop-on-notice`: Stop after first test that triggered a notice.\n- `--stop-on-skipped`: Stop execution upon first skipped test.\n- `--stop-on-incomplete`: Stop execution upon first incomplete test.\n- `--fail-on-empty-test-suite`: Signal failure using shell exit code when no tests were run.\n- `--fail-on-warning`: Treat tests with warnings as failures.\n- `--fail-on-risky`: Treat risky tests as failures.\n- `--fail-on-deprecation`: Signal failure using shell exit code when a deprecation was triggered.\n- `--fail-on-phpunit-deprecation`: Signal failure using shell exit code when a PHPUnit deprecation was triggered.\n- `--fail-on-notice`: Signal failure using shell exit code when a notice was triggered.\n- `--fail-on-skipped`: Treat skipped tests as failures.\n- `--fail-on-incomplete`: Signal failure using shell exit code when a test was marked incomplete.\n- `--cache-result`: Write test results to cache file.\n- `--do-not-cache-result`: Do not write test results to cache file\n- `--order-by <order>`: Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size.\n- `--random-order-seed <N>`: Use the specified random seed when running tests in random order\n\n## Reporting\n\n- `--colors <flag>`: Use colors in output (\"never\", \"auto\" or \"always\").\n- `--columns <n>`: Number of columns to use for progress output.\n- `--columns max`: Use maximum number of columns for progress output.\n- `--stderr`: Write to STDERR instead of STDOUT.\n- `--no-progress`: Disable output of test execution progress.\n- `--no-results`: Disable output of test results.\n- `--no-output`: Disable all output.\n- `--display-incomplete`: Display details for incomplete tests.\n- `--display-skipped`: Display details for skipped tests.\n- `--display-deprecations`: Display details for deprecations triggered by tests.\n- `--display-phpunit-deprecations`: Display details for PHPUnit deprecations.\n- `--display-errors`: Display details for errors triggered by tests.\n- `--display-notices`: Display details for notices triggered by tests.\n- `--display-warnings`: Display details for warnings triggered by tests.\n- `--reverse-list`: Print defects in reverse order.\n- `--teamcity`: Replace default progress and result output with TeamCity format.\n- `--testdox`: Replace default result output with TestDox format\n- `--testdox-summary`: Repeat TestDox output for tests with errors, failures, or issues.\n- `--debug`: Replace default progress and result output with debugging information.\n- `--compact`:  Replace default result output with Compact format\n\n## Logging\n\n- `--log-junit <file>`: Write test results in JUnit XML format to file.\n- `--log-teamcity <file>`: Write test results in TeamCity format to file.\n- `--testdox-html <file>`: Write test results in TestDox format (HTML) to file.\n- `--testdox-text <file>`: Write test results in TestDox format (plain text) to file.\n- `--log-events-text <file>`: Stream events as plain text to file.\n- `--log-events-verbose-text <file>`: Stream events as plain text (with telemetry information) to file.\n- `--no-logging`: Ignore logging configured in the XML configuration file\n\n## Code Coverage\n\n- `--coverage`: Generate code coverage report and output to standard output.\n- `--coverage --min=<value>`: Set the minimum required coverage percentage, and fail if not met.\n- `--coverage --exactly=<value>`: Set the exact required coverage percentage, and fail if not met.\n- `--coverage --only-covered`: Hide files with 0% coverage from the code coverage report.\n- `--coverage-clover <file>`: Write code coverage report in Clover XML format to file.\n- `--coverage-cobertura <file>`: Write code coverage report in Cobertura XML format to file.\n- `--coverage-crap4j <file>`: Write code coverage report in Crap4J XML format to file.\n- `--coverage-html <dir>`: Write code coverage report in HTML format to directory.\n- `--coverage-php <file>`: Write serialized code coverage data to file.\n- `--coverage-text=<file>`: Write code coverage report in text format to file [default: standard output].\n- `--only-summary-for-coverage-text`: Option for code coverage report in text format: only show summary.\n- `--show-uncovered-for-coverage-text`: Option for code coverage report in text format: show uncovered files.\n- `--coverage-xml <dir>`: Write code coverage report in XML format to directory.\n- `--warm-coverage-cache`: Warm static analysis cache.\n- `--coverage-filter <dir>`: Include `<dir>`: in code coverage reporting.\n- `--path-coverage`: Report path coverage in addition to line coverage.\n- `--disable-coverage-ignore`: Disable metadata for ignoring code coverage.\n- `--no-coverage`: Ignore code coverage reporting configured in the XML configuration file\n\n## Mutation Testing\n\n- `--mutate`: Runs mutation testing, to understand the quality of your tests.\n- `--mutate --parallel`: Runs mutation testing in parallel.\n- `--mutate --min`: Set the minimum required mutation score, and fail if not met.\n- `--mutate --id`:  Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run.\n- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.\n- `--mutate --bail`: Stop mutation testing execution upon first untested or uncovered mutation.\n- `--mutate --class`: Generate mutations for the given class(es). E.g. --class=App\\\\Models.\n- `--mutate --ignore`: Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests.\n- `--mutate --clear-cache`: Clear the mutation cache.\n- `--mutate --no-cache`: Clear the mutation cache.\n- `--mutate --ignore-min-score-on-zero-mutations`: Ignore the minimum score requirement when there are no mutations.\n- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.\n- `--mutate --everything`: Generate mutations for all classes, even if they are not covered by tests.\n- `--mutate --profile`: Output to standard output the top ten slowest mutations.\n- `--mutate --retry`: Run untested or uncovered mutations first and stop execution upon first error or failure.\n- `--mutate --stop-on-uncovered`: Stop mutation testing execution upon first untested mutation.\n- `--mutate --stop-on-untested`: Stop mutation testing execution upon first untested mutation.\n\n## Profiling\n\n- `--profile`: Output to standard output the top ten slowest tests\n\n---\n\nIn this chapter, you found a complete list of CLI options provided by Pest. In the subsequent documentation, we will explore the topic of test dependencies: [Test Dependencies](/docs/test-dependencies)\n\n\n---\n\n## Custom Expectations\nCustom Expectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/custom-expectations.md\n\n---\ntitle: Custom Expectations\ndescription: Pest's expectation API is powerful by default, but there may be times when you need to write the same expectations repeatedly between tests. In such cases, creating custom expectations that meet your specific requirements can be incredibly useful.\n---\n\n# Custom Expectations\n\nPest's expectation API is powerful by default, but there may be times when you need to write the same expectations repeatedly between tests. In such cases, creating custom expectations that meet your specific requirements can be incredibly useful.\n\nCustom expectations are usually defined in the `tests/Pest.php` file, but you can also organize them in a separate `tests/Expectations.php` file for better maintainability. To create a custom expectation in Pest, chain the `extend()` method onto the `expect()` function without providing any expectation value.\n\nFor example, suppose you are testing a number utility library and you need to frequently assert that numbers fall within a given range. In this case, you might create a custom expectation called `toBeWithinRange()`:\n\n```php\n// Pest.php or Expectations.php\nexpect()->extend('toBeWithinRange', function (int $min, int $max) {\n    return $this->toBeGreaterThanOrEqual($min)\n                ->toBeLessThanOrEqual($max);\n});\n\n// Tests/Unit/ExampleTest.php\ntest('numeric ranges', function () {\n    expect(100)->toBeWithinRange(90, 110);\n});\n```\n\nWhile users typically utilize Pest's built-in expectations within their custom expectations as demonstrated in the `toBeWithinRange()` example, there may be times when you need to access the expectation value directly to perform your own custom expectation logic. In such cases, you can access the expectation value that was passed to `expect($value)` via the `$this->value` property.\n\n```php\nexpect()->extend('toBeWithinRange', function (int $min, int $max) {\n    echo $this->value; // 100\n});\n```\n\nOf course, you probably want users to have the ability to \"chain\" expectations together with your custom expectation. To achieve this, ensure your custom expectation includes a `return $this` statement.\n\n```php\n// Pest.php or Expectations.php\nexpect()->extend('toBeWithinRange', function (int $min, int $max) {\n    // Assertions based on `$this->value` and the given arguments...\n\n    return $this; // Return this, so another expectations can chain this one...\n});\n\n// Tests/Unit/ExampleTest.php\ntest('numeric ranges', function () {\n    expect(100)\n        ->toBeInt()\n        ->toBeWithinRange(90, 110)\n        ->to...\n});\n```\n\nSometimes, you may need to trigger a test failure in your Custom Expectation. To do so, use the `test()` method in combination with the [`fail()`](/docs/exceptions) method.\n\n```php\n// Pest.php or Expectations.php\nexpect()->extend('toBeDivisibleBy', function (int $divisor) {\n    if ($divisor === 0) {\n        test()->fail('The divisor cannot be 0.');\n    }\n\n    return expect($this->value % $divisor)->toBe(0);\n});\n\n// Tests/Unit/ExampleTest.php\ntest('numeral division', function () {\n    expect(10)->toBeDivisibleBy(2); // Pass\n    expect(10)->toBeDivisibleBy(0); // Fail \"The divisor cannot be 0.\"\n});\n```\n\n## Intercept Expectations\n\nAlthough it is considered an advanced practice, you can override existing expectations with your own implementation via the `intercept()` method. When using this method, the existing expectation will be fully substituted if the expectation value is of the specified type. For example, you can replace the `toBe()` expectation to check if two objects of the `Illuminate\\Database\\Eloquent\\Model` type have the same `id`.\n\n```php\nuse Illuminate\\Database\\Eloquent\\Model;\nuse App\\Models\\User;\n\n// tests/Pest.php or tests/Expectations.php\nexpect()->intercept('toBe', Model::class, function(Model $expected) {\n    expect($this->value->id)->toBe($expected->id);\n});\n\n// tests/Feature/ExampleTest.php\ntest('models', function () {\n    $userA = User::find(1);\n    $userB = User::find(1);\n\n    expect($userA)->toBe($userB);\n});\n```\n\nInstead of passing a string type as the second argument to the `intercept()` method, you may also pass a closure, which will be invoked to determine whether or not to override the core expectation.\n\n```php\nexpect()->intercept('toBe', fn (mixed $value) => is_string($value), function (string $expected, bool $ignoreCase = false) {\n    if ($ignoreCase) {\n        assertEqualsIgnoringCase($expected, $this->value);\n    } else {\n        assertSame($expected, $this->value);\n    }\n});\n```\n\n## Pipe Expectations\n\nThere may be instances where you want to run one of Pest's built-in expectations, but include customized expectation logic under certain conditions. In these cases, you can use the `pipe()` method. For example, we may want to customize the behavior of the `toBe()` expectation if the given value is an Eloquent model.\n\n```php\nuse Illuminate\\Database\\Eloquent\\Model;\nuse App\\Models\\User;\n\nexpect()->pipe('toBe', function (Closure $next, mixed $expected) {\n    if ($this->value instanceof Model) {\n        return expect($this->value->id)->toBe($expected->id);\n    }\n\n    return $next(); // Run to the original, built-in expectation...\n});\n```\n\n---\n\nAs demonstrated, creating custom expectations can significantly simplify your code by eliminating the need to duplicate the logic to verify that your tests are behaving as anticipated. In the following chapter, we will explore how Pest can scan for Profanity, to help you maintain a professional codebase: [Profanity Plugin](/docs/profanity)\n\n\n_[Truncated]_"}]


cmd: sed -n '1,220p' app/Models/OauthToken.php && sed -n '1,220p' app/Livewire/Admin/Developers/Index.php


cmd: sed -n '1,220p' routes/api.php && sed -n '1,220p' bootstrap/app.php


cmd: sed -n '1,260p' tests/Feature/Api/AdminOrderApiTest.php && sed -n '1,220p' database/factories/OauthTokenFactory.php


command: sed -n '1,220p' routes/api.php && sed -n '1,220p' bootstrap/app.php

api.php

type: read

cmd: sed -n '1,220p' routes/api.php

app.php

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

aggregated_output:
<?php

use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
use App\Http\Controllers\Api\Storefront\V1\AnalyticsEventController as StorefrontAnalyticsEventController;
use App\Http\Controllers\Api\Storefront\V1\CartController;
use App\Http\Controllers\Api\Storefront\V1\CartLineController;
use App\Http\Controllers\Api\Storefront\V1\CheckoutController;
use App\Http\Controllers\Api\Storefront\V1\OrderController as StorefrontOrderController;
use App\Http\Controllers\Api\Storefront\V1\SearchController as StorefrontSearchController;
use Illuminate\Support\Facades\Route;

Route::middleware('store.resolve')
    ->prefix('storefront/v1')
    ->name('api.storefront.v1.')
    ->group(function (): void {
        Route::middleware('throttle:api.storefront')->group(function (): void {
            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
        });

        Route::middleware('throttle:search')->group(function (): void {
            Route::get('search', [StorefrontSearchController::class, 'index'])->name('search.index');
            Route::get('search/suggest', [StorefrontSearchController::class, 'suggest'])->name('search.suggest');
        });

        Route::middleware('throttle:analytics')->group(function (): void {
            Route::post('analytics/events', [StorefrontAnalyticsEventController::class, 'store'])->name('analytics.events.store');
        });

        Route::middleware('throttle:checkout')->group(function (): void {
            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
            Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show'])->name('orders.show');
        });
    });

Route::middleware(['auth', 'throttle:60,1'])
    ->prefix('admin/v1/stores/{store}')
    ->name('api.admin.v1.')
    ->group(function (): void {
        Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
        Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
        Route::post('orders/{order}/fulfillments', [AdminOrderFulfillmentController::class, 'store'])->name('orders.fulfillments.store');
        Route::post('orders/{order}/refunds', [AdminOrderRefundController::class, 'store'])->name('orders.refunds.store');
    });
<?php

use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\ResolveStore;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->alias([
            'role.check' => CheckStoreRole::class,
            'store.resolve' => ResolveStore::class,
        ]);

        $middleware->appendToGroup('storefront', [
            ResolveStore::class,
        ]);

        $middleware->appendToGroup('admin', [
            ResolveStore::class,
            CheckStoreRole::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

status: completed


command: sed -n '1,220p' app/Models/OauthToken.php && sed -n '1,220p' app/Livewire/Admin/Developers/Index.php

Index.php

aggregated_output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class OauthToken extends Model
{
    /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
    use HasFactory;

    public const UPDATED_AT = null;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'installation_id',
        'name',
        'access_token_hash',
        'refresh_token_hash',
        'abilities_json',
        'expires_at',
        'last_used_at',
        'created_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'abilities_json' => '[]',
    ];

    /**
     * @return BelongsTo<AppInstallation, $this>
     */
    public function installation(): BelongsTo
    {
        return $this->belongsTo(AppInstallation::class, 'installation_id');
    }

    public function isExpired(): bool
    {
        return $this->expires_at->isPast();
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'abilities_json' => 'array',
            'expires_at' => 'datetime',
            'last_used_at' => 'datetime',
            'created_at' => 'datetime',
        ];
    }
}
<?php

namespace App\Livewire\Admin\Developers;

use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookSubscription;
use App\Services\WebhookService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $newTokenName = '';

    public ?string $generatedToken = null;

    public string $webhookEventType = 'order.created';

    public string $webhookUrl = '';

    public ?int $editingWebhookId = null;

    /**
     * @var list<string>
     */
    public array $tokenAbilities = [
        'read-products',
        'write-products',
        'read-orders',
        'write-orders',
        'read-customers',
        'write-customers',
        'read-analytics',
    ];

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);
        $this->storeId = $store->getKey();
    }

    public function generateToken(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'newTokenName' => ['required', 'string', 'max:255'],
        ], [], [
            'newTokenName' => 'token name',
        ]);

        $result = $webhooks->createApiToken($this->scopedStore(), $this->newTokenName, $this->tokenAbilities);

        $this->generatedToken = $result['plain_text'];
        $this->newTokenName = '';
        $this->modal('generate-token')->close();
        $this->dispatch('toast', type: 'success', message: __('API token generated'));
    }

    public function revokeToken(int $tokenId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->token($tokenId)->delete();

        session()->flash('status', __('API token revoked'));
        $this->dispatch('toast', type: 'success', message: __('API token revoked'));
    }

    public function openWebhookModal(?int $webhookId = null): void
    {
        $this->editingWebhookId = $webhookId;

        if ($webhookId !== null) {
            $webhook = $this->webhook($webhookId);
            $this->webhookEventType = $webhook->event_type->value;
            $this->webhookUrl = $webhook->target_url;
        } else {
            $this->webhookEventType = WebhookEventType::OrderCreated->value;
            $this->webhookUrl = '';
        }

        $this->modal('webhook-form')->show();
    }

    public function saveWebhook(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'webhookEventType' => ['required', Rule::enum(WebhookEventType::class)],
            'webhookUrl' => ['required', 'url', 'max:2048'],
        ], [], [
            'webhookEventType' => 'event type',
            'webhookUrl' => 'endpoint URL',
        ]);

        $webhook = $this->editingWebhookId !== null
            ? $this->webhook($this->editingWebhookId)
            : new WebhookSubscription([
                'store_id' => $this->storeId,
                'signing_secret_encrypted' => $webhooks->createSigningSecret(),
            ]);

        $webhook->forceFill([
            'event_type' => $this->webhookEventType,
            'target_url' => $this->webhookUrl,
            'status' => WebhookSubscriptionStatus::Active,
        ])->save();

        $this->editingWebhookId = null;
        $this->webhookUrl = '';
        $this->modal('webhook-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Webhook saved'));
    }

    public function deleteWebhook(int $webhookId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->webhook($webhookId)->delete();
        $this->dispatch('toast', type: 'success', message: __('Webhook deleted'));
    }

    /**
     * @return Collection<int, OauthToken>
     */
    public function tokens(): Collection
    {
        return OauthToken::query()
            ->with('installation.app')
            ->whereHas('installation', function ($query): void {
                $query->withoutGlobalScopes()->where('store_id', $this->storeId);
            })
            ->latest('created_at')
            ->get();
    }

    /**
     * @return Collection<int, WebhookSubscription>
     */
    public function webhooks(): Collection
    {
        return WebhookSubscription::withoutGlobalScopes()
            ->withCount([
                'deliveries',
                'deliveries as failed_deliveries_count' => fn ($query) => $query->where('status', 'failed'),
            ])
            ->where('store_id', $this->storeId)
            ->orderBy('event_type')
            ->get();
    }

    public function render()
    {
        return view('livewire.admin.developers.index', [
            'tokens' => $this->tokens(),
            'webhooks' => $this->webhooks(),
            'eventTypes' => WebhookEventType::selectable(),
        ])->layout('layouts.app', [
            'title' => __('Developers'),
        ]);
    }

    private function token(int $tokenId): OauthToken
    {
        return OauthToken::query()
            ->whereHas('installation', function ($query): void {
                $query->withoutGlobalScopes()->where('store_id', $this->storeId);
            })
            ->findOrFail($tokenId);
    }

    private function webhook(int $webhookId): WebhookSubscription
    {
        return WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->findOrFail($webhookId);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }
}


command: sed -n '1,260p' tests/Feature/Api/AdminOrderApiTest.php && sed -n '1,220p' database/factories/OauthTokenFactory.php

OauthTokenFactory.php

aggregated_output:
<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\PaymentStatus;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
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;

uses(RefreshDatabase::class);

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

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

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

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderApiOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

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

    $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,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin API Product',
        'sku_snapshot' => 'ADMIN-API-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order api lists and shows store scoped orders', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store, [
        'order_number' => '#8001',
        'email' => 'alpha@example.test',
    ]);
    $otherStore = Store::factory()->create();
    adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=alpha")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8001')
        ->assertJsonMissing(['order_number' => '#9001']);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.order_number', '#8001')
        ->assertJsonPath('data.lines.0.title', 'Admin API Product');
});

test('admin order api creates refunds and fulfillments', function (): void {
    $store = adminOrderApiStore();
    [$order, $line] = adminOrderApiOrder($store, quantity: 2, unitPrice: 2500);
    $user = adminOrderApiUser();

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 1000,
            'reason' => 'Customer return',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 1000)
        ->assertJsonPath('data.status', 'processed');

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded);

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/fulfillments", [
            'line_items' => [
                ['order_line_id' => $line->getKey(), 'quantity' => 1],
            ],
            'tracking_company' => 'DHL',
            'tracking_number' => 'DHL123',
        ])
        ->assertCreated()
        ->assertJsonPath('data.status', 'pending')
        ->assertJsonPath('data.lines.0.quantity', 1);

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->tracking_number)->toBe('DHL123');
});

test('admin order api rejects orders outside the requested store', function (): void {
    $store = adminOrderApiStore();
    $otherStore = Store::factory()->create();
    [$otherOrder] = adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
    ]);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$otherOrder->getKey()}")
        ->assertNotFound();
});
<?php

namespace Database\Factories;

use App\Models\AppInstallation;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OauthToken>
 */
class OauthTokenFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $token = 'shop_'.Str::random(48);

        return [
            'installation_id' => AppInstallation::factory(),
            'name' => fake()->words(2, true),
            'access_token_hash' => hash('sha256', $token),
            'refresh_token_hash' => hash('sha256', 'refresh_'.Str::random(48)),
            'abilities_json' => ['read-products', 'read-orders'],
            'expires_at' => now()->addYear(),
            'last_used_at' => null,
            'created_at' => now(),
        ];
    }
}


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

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class OauthToken extends Model
{
    /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
    use HasFactory;

    public const UPDATED_AT = null;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'installation_id',
        'name',
        'access_token_hash',
        'refresh_token_hash',
        'abilities_json',
        'expires_at',
        'last_used_at',
        'created_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'abilities_json' => '[]',
    ];

    /**
     * @return BelongsTo<AppInstallation, $this>
     */
    public function installation(): BelongsTo
    {
        return $this->belongsTo(AppInstallation::class, 'installation_id');
    }

    public function isExpired(): bool
    {
        return $this->expires_at->isPast();
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'abilities_json' => 'array',
            'expires_at' => 'datetime',
            'last_used_at' => 'datetime',
            'created_at' => 'datetime',
        ];
    }
}
<?php

namespace App\Livewire\Admin\Developers;

use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookSubscription;
use App\Services\WebhookService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $newTokenName = '';

    public ?string $generatedToken = null;

    public string $webhookEventType = 'order.created';

    public string $webhookUrl = '';

    public ?int $editingWebhookId = null;

    /**
     * @var list<string>
     */
    public array $tokenAbilities = [
        'read-products',
        'write-products',
        'read-orders',
        'write-orders',
        'read-customers',
        'write-customers',
        'read-analytics',
    ];

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);
        $this->storeId = $store->getKey();
    }

    public function generateToken(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'newTokenName' => ['required', 'string', 'max:255'],
        ], [], [
            'newTokenName' => 'token name',
        ]);

        $result = $webhooks->createApiToken($this->scopedStore(), $this->newTokenName, $this->tokenAbilities);

        $this->generatedToken = $result['plain_text'];
        $this->newTokenName = '';
        $this->modal('generate-token')->close();
        $this->dispatch('toast', type: 'success', message: __('API token generated'));
    }

    public function revokeToken(int $tokenId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->token($tokenId)->delete();

        session()->flash('status', __('API token revoked'));
        $this->dispatch('toast', type: 'success', message: __('API token revoked'));
    }

    public function openWebhookModal(?int $webhookId = null): void
    {
        $this->editingWebhookId = $webhookId;

        if ($webhookId !== null) {
            $webhook = $this->webhook($webhookId);
            $this->webhookEventType = $webhook->event_type->value;
            $this->webhookUrl = $webhook->target_url;
        } else {
            $this->webhookEventType = WebhookEventType::OrderCreated->value;
            $this->webhookUrl = '';
        }

        $this->modal('webhook-form')->show();
    }

    public function saveWebhook(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'webhookEventType' => ['required', Rule::enum(WebhookEventType::class)],
            'webhookUrl' => ['required', 'url', 'max:2048'],
        ], [], [
            'webhookEventType' => 'event type',
            'webhookUrl' => 'endpoint URL',
        ]);

        $webhook = $this->editingWebhookId !== null
            ? $this->webhook($this->editingWebhookId)
            : new WebhookSubscription([
                'store_id' => $this->storeId,
                'signing_secret_encrypted' => $webhooks->createSigningSecret(),
            ]);

        $webhook->forceFill([
            'event_type' => $this->webhookEventType,
            'target_url' => $this->webhookUrl,
            'status' => WebhookSubscriptionStatus::Active,
        ])->save();

        $this->editingWebhookId = null;
        $this->webhookUrl = '';
        $this->modal('webhook-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Webhook saved'));
    }

    public function deleteWebhook(int $webhookId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->webhook($webhookId)->delete();
        $this->dispatch('toast', type: 'success', message: __('Webhook deleted'));
    }

    /**
     * @return Collection<int, OauthToken>
     */
    public function tokens(): Collection
    {
        return OauthToken::query()
            ->with('installation.app')
            ->whereHas('installation', function ($query): void {
                $query->withoutGlobalScopes()->where('store_id', $this->storeId);
            })
            ->latest('created_at')
            ->get();
    }

    /**
     * @return Collection<int, WebhookSubscription>
     */
    public function webhooks(): Collection
    {
        return WebhookSubscription::withoutGlobalScopes()
            ->withCount([
                'deliveries',
                'deliveries as failed_deliveries_count' => fn ($query) => $query->where('status', 'failed'),
            ])
            ->where('store_id', $this->storeId)
            ->orderBy('event_type')
            ->get();
    }

    public function render()
    {
        return view('livewire.admin.developers.index', [
            'tokens' => $this->tokens(),
            'webhooks' => $this->webhooks(),
            'eventTypes' => WebhookEventType::selectable(),
        ])->layout('layouts.app', [
            'title' => __('Developers'),
        ]);
    }

    private function token(int $tokenId): OauthToken
    {
        return OauthToken::query()
            ->whereHas('installation', function ($query): void {
                $query->withoutGlobalScopes()->where('store_id', $this->storeId);
            })
            ->findOrFail($tokenId);
    }

    private function webhook(int $webhookId): WebhookSubscription
    {
        return WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->findOrFail($webhookId);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }
}


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

use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
use App\Http\Controllers\Api\Storefront\V1\AnalyticsEventController as StorefrontAnalyticsEventController;
use App\Http\Controllers\Api\Storefront\V1\CartController;
use App\Http\Controllers\Api\Storefront\V1\CartLineController;
use App\Http\Controllers\Api\Storefront\V1\CheckoutController;
use App\Http\Controllers\Api\Storefront\V1\OrderController as StorefrontOrderController;
use App\Http\Controllers\Api\Storefront\V1\SearchController as StorefrontSearchController;
use Illuminate\Support\Facades\Route;

Route::middleware('store.resolve')
    ->prefix('storefront/v1')
    ->name('api.storefront.v1.')
    ->group(function (): void {
        Route::middleware('throttle:api.storefront')->group(function (): void {
            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
        });

        Route::middleware('throttle:search')->group(function (): void {
            Route::get('search', [StorefrontSearchController::class, 'index'])->name('search.index');
            Route::get('search/suggest', [StorefrontSearchController::class, 'suggest'])->name('search.suggest');
        });

        Route::middleware('throttle:analytics')->group(function (): void {
            Route::post('analytics/events', [StorefrontAnalyticsEventController::class, 'store'])->name('analytics.events.store');
        });

        Route::middleware('throttle:checkout')->group(function (): void {
            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
            Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show'])->name('orders.show');
        });
    });

Route::middleware(['auth', 'throttle:60,1'])
    ->prefix('admin/v1/stores/{store}')
    ->name('api.admin.v1.')
    ->group(function (): void {
        Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
        Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
        Route::post('orders/{order}/fulfillments', [AdminOrderFulfillmentController::class, 'store'])->name('orders.fulfillments.store');
        Route::post('orders/{order}/refunds', [AdminOrderRefundController::class, 'store'])->name('orders.refunds.store');
    });
<?php

use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\ResolveStore;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->alias([
            'role.check' => CheckStoreRole::class,
            'store.resolve' => ResolveStore::class,
        ]);

        $middleware->appendToGroup('storefront', [
            ResolveStore::class,
        ]);

        $middleware->appendToGroup('admin', [
            ResolveStore::class,
            CheckStoreRole::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();


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

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\PaymentStatus;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
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;

uses(RefreshDatabase::class);

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

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

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

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderApiOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

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

    $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,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin API Product',
        'sku_snapshot' => 'ADMIN-API-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order api lists and shows store scoped orders', function (): void {
    $store = adminOrderApiStore();
    [$order] = adminOrderApiOrder($store, [
        'order_number' => '#8001',
        'email' => 'alpha@example.test',
    ]);
    $otherStore = Store::factory()->create();
    adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->getJson("/api/admin/v1/stores/{$store->getKey()}/orders")
        ->assertUnauthorized();

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders?query=alpha")
        ->assertOk()
        ->assertJsonPath('data.0.order_number', '#8001')
        ->assertJsonMissing(['order_number' => '#9001']);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}")
        ->assertOk()
        ->assertJsonPath('data.order_number', '#8001')
        ->assertJsonPath('data.lines.0.title', 'Admin API Product');
});

test('admin order api creates refunds and fulfillments', function (): void {
    $store = adminOrderApiStore();
    [$order, $line] = adminOrderApiOrder($store, quantity: 2, unitPrice: 2500);
    $user = adminOrderApiUser();

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/refunds", [
            'amount' => 1000,
            'reason' => 'Customer return',
        ])
        ->assertCreated()
        ->assertJsonPath('data.amount', 1000)
        ->assertJsonPath('data.status', 'processed');

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded);

    $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$order->getKey()}/fulfillments", [
            'line_items' => [
                ['order_line_id' => $line->getKey(), 'quantity' => 1],
            ],
            'tracking_company' => 'DHL',
            'tracking_number' => 'DHL123',
        ])
        ->assertCreated()
        ->assertJsonPath('data.status', 'pending')
        ->assertJsonPath('data.lines.0.quantity', 1);

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->tracking_number)->toBe('DHL123');
});

test('admin order api rejects orders outside the requested store', function (): void {
    $store = adminOrderApiStore();
    $otherStore = Store::factory()->create();
    [$otherOrder] = adminOrderApiOrder($otherStore, [
        'order_number' => '#9001',
    ]);

    $this->actingAs(adminOrderApiUser())
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/orders/{$otherOrder->getKey()}")
        ->assertNotFound();
});
<?php

namespace Database\Factories;

use App\Models\AppInstallation;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OauthToken>
 */
class OauthTokenFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $token = 'shop_'.Str::random(48);

        return [
            'installation_id' => AppInstallation::factory(),
            'name' => fake()->words(2, true),
            'access_token_hash' => hash('sha256', $token),
            'refresh_token_hash' => hash('sha256', 'refresh_'.Str::random(48)),
            'abilities_json' => ['read-products', 'read-orders'],
            'expires_at' => now()->addYear(),
            'last_used_at' => null,
            'created_at' => now(),
        ];
    }
}


cmd: rg "createApiToken|plain_text|access_token_hash" -n app database tests


cmd: sed -n '1,220p' app/Models/AppInstallation.php && sed -n '1,220p' database/factories/AppInstallationFactory.php


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


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

aggregated_output:
<?php

namespace App\Services;

use App\Enums\AppInstallationStatus;
use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\App as AppModel;
use App\Models\AppInstallation;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: OauthToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities): array
    {
        $app = AppModel::query()->firstOrCreate(
            ['name' => 'Admin API'],
            [
                'status' => 'active',
                'created_at' => now(),
            ],
        );

        $installation = AppInstallation::withoutGlobalScopes()->firstOrCreate(
            [
                'store_id' => $store->getKey(),
                'app_id' => $app->getKey(),
            ],
            [
                'scopes_json' => $abilities,
                'status' => AppInstallationStatus::Active,
                'installed_at' => now(),
            ],
        );

        $plainText = 'shop_'.Str::random(48);
        $token = OauthToken::query()->create([
            'installation_id' => $installation->getKey(),
            'name' => $name,
            'access_token_hash' => hash('sha256', $plainText),
            'refresh_token_hash' => null,
            'abilities_json' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


command: sed -n '1,220p' app/Models/AppInstallation.php && sed -n '1,220p' database/factories/AppInstallationFactory.php

AppInstallationFactory.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\AppInstallationStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class AppInstallation extends Model
{
    /** @use HasFactory<\Database\Factories\AppInstallationFactory> */
    use BelongsToStore, HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'app_id',
        'scopes_json',
        'status',
        'installed_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'scopes_json' => '[]',
        'status' => 'active',
    ];

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

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

    /**
     * @return HasMany<OauthToken, $this>
     */
    public function oauthTokens(): HasMany
    {
        return $this->hasMany(OauthToken::class, 'installation_id');
    }

    /**
     * @return HasMany<WebhookSubscription, $this>
     */
    public function webhookSubscriptions(): HasMany
    {
        return $this->hasMany(WebhookSubscription::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'scopes_json' => 'array',
            'status' => AppInstallationStatus::class,
            'installed_at' => 'datetime',
        ];
    }
}
<?php

namespace Database\Factories;

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

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AppInstallation>
 */
class AppInstallationFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'app_id' => App::factory(),
            'scopes_json' => ['read-products', 'read-orders'],
            'status' => AppInstallationStatus::Active,
            'installed_at' => now(),
        ];
    }
}


command: rg "createApiToken|plain_text|access_token_hash" -n app database tests

type: search

cmd: rg 'createApiToken|plain_text|access_token_hash' -n app database tests

query: createApiToken|plain_text|access_token_hash

path: app

aggregated_output:
database/factories/OauthTokenFactory.php:26:            'access_token_hash' => hash('sha256', $token),
app/Models/OauthToken.php:22:        'access_token_hash',
app/Livewire/Admin/Developers/Index.php:65:        $result = $webhooks->createApiToken($this->scopedStore(), $this->newTokenName, $this->tokenAbilities);
app/Livewire/Admin/Developers/Index.php:67:        $this->generatedToken = $result['plain_text'];
app/Services/WebhookService.php:65:     * @return array{token: OauthToken, plain_text: string}
app/Services/WebhookService.php:67:    public function createApiToken(Store $store, string $name, array $abilities): array
app/Services/WebhookService.php:93:            'access_token_hash' => hash('sha256', $plainText),
app/Services/WebhookService.php:102:            'plain_text' => $plainText,
database/seeders/AppSeeder.php:81:                    'access_token_hash' => hash('sha256', 'shop_seed_'.$store->handle.'_'.Str::slug($app->name)),
database/migrations/2026_05_04_034611_create_oauth_tokens_table.php:18:            $table->string('access_token_hash')->unique('idx_oauth_tokens_access_hash');


output:
Chunk ID: d9af9e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 279
Output:
database/factories/OauthTokenFactory.php:26:            'access_token_hash' => hash('sha256', $token),
app/Models/OauthToken.php:22:        'access_token_hash',
app/Livewire/Admin/Developers/Index.php:65:        $result = $webhooks->createApiToken($this->scopedStore(), $this->newTokenName, $this->tokenAbilities);
app/Livewire/Admin/Developers/Index.php:67:        $this->generatedToken = $result['plain_text'];
app/Services/WebhookService.php:65:     * @return array{token: OauthToken, plain_text: string}
app/Services/WebhookService.php:67:    public function createApiToken(Store $store, string $name, array $abilities): array
app/Services/WebhookService.php:93:            'access_token_hash' => hash('sha256', $plainText),
app/Services/WebhookService.php:102:            'plain_text' => $plainText,
database/seeders/AppSeeder.php:81:                    'access_token_hash' => hash('sha256', 'shop_seed_'.$store->handle.'_'.Str::slug($app->name)),
database/migrations/2026_05_04_034611_create_oauth_tokens_table.php:18:            $table->string('access_token_hash')->unique('idx_oauth_tokens_access_hash');


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

namespace App\Models;

use App\Enums\AppInstallationStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class AppInstallation extends Model
{
    /** @use HasFactory<\Database\Factories\AppInstallationFactory> */
    use BelongsToStore, HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'app_id',
        'scopes_json',
        'status',
        'installed_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'scopes_json' => '[]',
        'status' => 'active',
    ];

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

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

    /**
     * @return HasMany<OauthToken, $this>
     */
    public function oauthTokens(): HasMany
    {
        return $this->hasMany(OauthToken::class, 'installation_id');
    }

    /**
     * @return HasMany<WebhookSubscription, $this>
     */
    public function webhookSubscriptions(): HasMany
    {
        return $this->hasMany(WebhookSubscription::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'scopes_json' => 'array',
            'status' => AppInstallationStatus::class,
            'installed_at' => 'datetime',
        ];
    }
}
<?php

namespace Database\Factories;

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

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AppInstallation>
 */
class AppInstallationFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'app_id' => App::factory(),
            'scopes_json' => ['read-products', 'read-orders'],
            'status' => AppInstallationStatus::Active,
            'installed_at' => now(),
        ];
    }
}


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

namespace App\Services;

use App\Enums\AppInstallationStatus;
use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\App as AppModel;
use App\Models\AppInstallation;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: OauthToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities): array
    {
        $app = AppModel::query()->firstOrCreate(
            ['name' => 'Admin API'],
            [
                'status' => 'active',
                'created_at' => now(),
            ],
        );

        $installation = AppInstallation::withoutGlobalScopes()->firstOrCreate(
            [
                'store_id' => $store->getKey(),
                'app_id' => $app->getKey(),
            ],
            [
                'scopes_json' => $abilities,
                'status' => AppInstallationStatus::Active,
                'installed_at' => now(),
            ],
        );

        $plainText = 'shop_'.Str::random(48);
        $token = OauthToken::query()->create([
            'installation_id' => $installation->getKey(),
            'name' => $name,
            'access_token_hash' => hash('sha256', $plainText),
            'refresh_token_hash' => null,
            'abilities_json' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


cmd: sed -n '1,260p' app/Http/Controllers/Api/Admin/V1/OrderController.php && sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php && sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/OrderRefundController.php


cmd: sed -n '1,220p' app/Http/Requests/Api/Admin/V1/CreateFulfillmentRequest.php && sed -n '1,220p' app/Http/Requests/Api/Admin/V1/CreateRefundRequest.php


cmd: sed -n '1,200p' database/migrations/2026_05_04_034611_create_oauth_tokens_table.php && sed -n '1,200p' app/Enums/AppInstallationStatus.php


command: sed -n '1,260p' app/Http/Controllers/Api/Admin/V1/OrderController.php && sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php && sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/OrderRefundController.php

OrderRefundController.php

aggregated_output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\OrderResource;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\Rule;

class OrderController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in(['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])],
            'financial_status' => ['nullable', Rule::in(['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])],
            'fulfillment_status' => ['nullable', Rule::in(['unfulfilled', 'partial', 'fulfilled'])],
            'query' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $orders = Order::withoutGlobalScopes()
            ->with('customer')
            ->withCount('lines')
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'financial_status'), fn (Builder $query, string $status) => $query->where('financial_status', $status))
            ->when(data_get($validated, 'fulfillment_status'), fn (Builder $query, string $status) => $query->where('fulfillment_status', $status))
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $like = '%'.$search.'%';

                $query->where(function (Builder $query) use ($like): void {
                    $query
                        ->where('order_number', 'like', $like)
                        ->orWhere('email', 'like', $like)
                        ->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', $like));
                });
            })
            ->latest('placed_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return OrderResource::collection($orders);
    }

    public function show(Request $request, Store $store, Order $order): OrderResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessOrderBelongsToStore($order, $store);

        return OrderResource::make($this->loadOrder($order));
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);

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

    private function abortUnlessOrderBelongsToStore(Order $order, Store $store): void
    {
        abort_unless((int) $order->store_id === $store->getKey(), 404);
    }

    private function loadOrder(Order $order): Order
    {
        return $order->load([
            'customer',
            'lines',
            'payments',
            'refunds',
            'fulfillments.lines',
        ])->loadCount('lines');
    }
}
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Exceptions\InvalidFulfillmentOperationException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Admin\V1\CreateOrderFulfillmentRequest;
use App\Http\Resources\Admin\V1\FulfillmentResource;
use App\Models\Order;
use App\Models\Store;
use App\Services\FulfillmentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class OrderFulfillmentController extends Controller
{
    public function store(CreateOrderFulfillmentRequest $request, Store $store, Order $order, FulfillmentService $fulfillments): FulfillmentResource|JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessOrderBelongsToStore($order, $store);

        try {
            $fulfillment = $fulfillments->create($order, $request->lineQuantities(), [
                'tracking_company' => $request->validated('tracking_company'),
                'tracking_number' => $request->validated('tracking_number'),
                'tracking_url' => $request->validated('tracking_url'),
            ]);

            return FulfillmentResource::make($fulfillment->load('lines'))
                ->response()
                ->setStatusCode(201);
        } catch (InvalidFulfillmentOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 409);
        }
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);

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

    private function abortUnlessOrderBelongsToStore(Order $order, Store $store): void
    {
        abort_unless((int) $order->store_id === $store->getKey(), 404);
    }
}
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Exceptions\InvalidRefundOperationException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Admin\V1\CreateOrderRefundRequest;
use App\Http\Resources\Admin\V1\RefundResource;
use App\Models\Order;
use App\Models\Store;
use App\Services\RefundService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class OrderRefundController extends Controller
{
    public function store(CreateOrderRefundRequest $request, Store $store, Order $order, RefundService $refunds): RefundResource|JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessOrderBelongsToStore($order, $store);

        $payload = [
            'lines' => $request->lineQuantities(),
            'reason' => $request->validated('reason'),
            'restock' => (bool) $request->validated('restock', false),
        ];

        if ($request->validated('amount') !== null) {
            $payload['amount'] = $request->validated('amount');
        }

        try {
            $refund = $refunds->process($order, $payload);

            return RefundResource::make($refund)
                ->response()
                ->setStatusCode(201);
        } catch (InvalidRefundOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 409);
        }
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);

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

    private function abortUnlessOrderBelongsToStore(Order $order, Store $store): void
    {
        abort_unless((int) $order->store_id === $store->getKey(), 404);
    }
}


command: sed -n '1,220p' app/Http/Requests/Api/Admin/V1/CreateFulfillmentRequest.php && sed -n '1,220p' app/Http/Requests/Api/Admin/V1/CreateRefundRequest.php

CreateRefundRequest.php

aggregated_output: sed: app/Http/Requests/Api/Admin/V1/CreateFulfillmentRequest.php: No such file or directory

status: failed


command: sed -n '1,200p' database/migrations/2026_05_04_034611_create_oauth_tokens_table.php && sed -n '1,200p' app/Enums/AppInstallationStatus.php

AppInstallationStatus.php

aggregated_output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('oauth_tokens', function (Blueprint $table) {
            $table->id();
            $table->foreignId('installation_id')->constrained('app_installations')->cascadeOnDelete();
            $table->string('name')->nullable();
            $table->string('access_token_hash')->unique('idx_oauth_tokens_access_hash');
            $table->string('refresh_token_hash')->nullable();
            $table->json('abilities_json')->default('[]');
            $table->timestamp('expires_at');
            $table->timestamp('last_used_at')->nullable();
            $table->timestamp('created_at')->nullable();

            $table->index('installation_id', 'idx_oauth_tokens_installation_id');
            $table->index('expires_at', 'idx_oauth_tokens_expires_at');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('oauth_tokens');
    }
};
<?php

namespace App\Enums;

enum AppInstallationStatus: string
{
    case Active = 'active';
    case Suspended = 'suspended';
    case Uninstalled = 'uninstalled';

    public function label(): string
    {
        return match ($this) {
            self::Active => 'Active',
            self::Suspended => 'Suspended',
            self::Uninstalled => 'Uninstalled',
        };
    }

    public function badgeColor(): string
    {
        return match ($this) {
            self::Active => 'green',
            self::Suspended => 'amber',
            self::Uninstalled => 'zinc',
        };
    }
}