Output:
Wall time: 3.3649 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Http Tests Testing JSON APIs\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n## Testing JSON APIs\n\nLaravel also provides several helpers for testing JSON APIs and their responses. For example, the `json`, `getJson`, `postJson`, `putJson`, `patchJson`, `deleteJson`, and `optionsJson` methods may be used to issue JSON requests with various HTTP verbs. You may also easily pass data and headers to these methods. To get started, let's write a test to make a `POST` request to `/api/user` and assert that the expected JSON data was returned:\n\n```php tab=Pest\n<?php\n\ntest('making an api request', function () {\n    $response = $this->postJson('/api/user', ['name' => 'Sally']);\n\n    $response\n        ->assertStatus(201)\n        ->assertJson([\n            'created' => true,\n        ]);\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    /**\n     * A basic functional test example.\n     */\n    public function test_making_an_api_request(): void\n    {\n        $response = $this->postJson('/api/user', ['name' => 'Sally']);\n\n        $response\n            ->assertStatus(201)\n            ->assertJson([\n                'created' => true,\n            ]);\n    }\n}\n```\n\nIn addition, JSON response data may be accessed as array variables on the response, making it convenient for you to inspect the individual values returned within a JSON response:\n\n```php tab=Pest\nexpect($response['created'])->toBeTrue();\n```\n\n```php tab=PHPUnit\n$this->assertTrue($response['created']);\n```\n\n> [!NOTE]\n> The `assertJson` method converts the response to an array to verify that the given array exists within the JSON response returned by the application. So, if there are other properties in the JSON response, this test will still pass as long as the given fragment is present.\n\n---\n\n## Stress Testing The Stress function Requests Upload Data Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Upload Data Rate\n\nReturns the request upload data rate in bytes per second.\n\n```php\n$result->requests()->upload()->data()->rate();\n```\n\n---\n\n## Expectations `json()`\nExpectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md\n\n\n### `json()`\n\nThe `json()` modifier decodes the current expectation `$value` from JSON to an array.\n\n```php\nexpect('{\"name\":\"Nuno\",\"credit\":1000.00}')\n    ->json()\n    ->toHaveCount(2)\n    ->name->toBe('Nuno')\n    ->credit->toBeFloat();\n\nexpect('not-a-json')->json(); //Fails\n```\n\n---\n\n## Http Tests Testing JSON APIs Fluent JSON Testing\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n### Fluent JSON Testing\n\nLaravel also offers a beautiful way to fluently test your application's JSON responses. To get started, pass a closure to the `assertJson` method. This closure will be invoked with an instance of `Illuminate\\Testing\\Fluent\\AssertableJson` which can be used to make assertions against the JSON that was returned by your application. The `where` method may be used to make assertions against a particular attribute of the JSON, while the `missing` method may be used to assert that a particular attribute is missing from the JSON:\n\n```php tab=Pest\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\n\ntest('fluent json', function () {\n    $response = $this->getJson('/users/1');\n\n    $response\n        ->assertJson(fn (AssertableJson $json) =>\n            $json->where('id', 1)\n                ->where('name', 'Victoria Faith')\n                ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))\n                ->whereNot('status', 'pending')\n                ->missing('password')\n                ->etc()\n        );\n});\n```\n\n```php tab=PHPUnit\nuse Illuminate\\Testing\\Fluent\\AssertableJson;\n\n/**\n * A basic functional test example.\n */\npublic function test_fluent_json(): void\n{\n    $response = $this->getJson('/users/1');\n\n    $response\n        ->assertJson(fn (AssertableJson $json) =>\n            $json->where('id', 1)\n                ->where('name', 'Victoria Faith')\n                ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))\n                ->whereNot('status', 'pending')\n                ->missing('password')\n                ->etc()\n        );\n}\n```\n\n#### Understanding the `etc` Method\n\nIn the example above, you may have noticed we invoked the `etc` method at the end of our assertion chain. This method informs Laravel that there may be other attributes present on the JSON object. If the `etc` method is not used, the test will fail if other attributes that you did not make assertions against exist on the JSON object.\n\nThe intention behind this behavior is to protect you from unintentionally exposing sensitive information in your JSON responses by forcing you to either explicitly make an assertion against the attribute or explicitly allow additional attributes via the `etc` method.\n\nHowever, you should be aware that not including the `etc` method in your assertion chain does not ensure that additional attributes are not being added to arrays that are nested within your JSON object. The `etc` method only ensures that no additional attributes exist at the nesting level in which the `etc` method is invoked.\n\n---\n\n## Routing Rate Limiting Attaching Rate Limiters to Routes\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Attaching Rate Limiters to Routes\n\nRate limiters may be attached to routes or route groups using the `throttle` [middleware](/docs/{{version}}/middleware). The throttle middleware accepts the name of the rate limiter you wish to assign to the route:\n\n```php\nRoute::middleware(['throttle:uploads'])->group(function () {\n    Route::post('/audio', function () {\n        // ...\n    });\n\n    Route::post('/video', function () {\n        // ...\n    });\n});\n```\n\n---\n\n## Validation Available Validation Rules json\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### json\n\nThe field under validation must be a valid JSON string.\n\n---\n\n## Stress Testing The Stress function Requests Download Data Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Download Data Rate\n\nReturns the request download data rate in bytes per second.\n\n```php\n$result->requests()->download()->data()->rate();\n```\n\n---\n\n## Expectations `toBeJson()`\nExpectations | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/expectations.md\n\n\n### `toBeJson()`\n\nThis expectation ensures that `$value` is a JSON string.\n\n```php\nexpect('{\"hello\":\"world\"}')->toBeJson();\n```\n\n---\n\n## Http Tests Testing JSON APIs Asserting on JSON Paths\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### Asserting on JSON Paths\n\nIf you would like to verify that the JSON response contains the given data at a specified path, you should use the `assertJsonPath` method:\n\n```php tab=Pest\n<?php\n\ntest('asserting a json path value', function () {\n    $response = $this->postJson('/user', ['name' => 'Sally']);\n\n    $response\n        ->assertStatus(201)\n        ->assertJsonPath('team.owner.name', 'Darian');\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    /**\n     * A basic functional test example.\n     */\n    public function test_asserting_a_json_paths_value(): void\n    {\n        $response = $this->postJson('/user', ['name' => 'Sally']);\n\n        $response\n            ->assertStatus(201)\n            ->assertJsonPath('team.owner.name', 'Darian');\n    }\n}\n```\n\nThe `assertJsonPath` method also accepts a closure, which may be used to dynamically determine if the assertion should pass:\n\n```php\n$response->assertJsonPath('team.owner.name', fn (string $name) => strlen($name) >= 3);\n```\n\n---\n\n## Eloquent Serialization Serializing Models and Collections Serializing to JSON Relationships\nEloquent Serialization | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-serialization.md\n\n\n#### Relationships\n\nWhen an Eloquent model is converted to JSON, its loaded relationships will automatically be included as attributes on the JSON object. Also, though Eloquent relationship methods are defined using \"camel case\" method names, a relationship's JSON attribute will be \"snake case\".\n\n---\n\n## Http Tests Testing JSON APIs Fluent JSON Testing Scoping JSON Collection Assertions\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### Scoping JSON Collection Assertions\n\nSometimes, your application's routes will return JSON collections that are assigned named keys:\n\n```php\nRoute::get('/users', function () {\n    return [\n        'meta' => [...],\n        'users' => User::all(),\n    ];\n})\n```\n\nWhen testing these routes, you may use the `has` method to assert against the number of items in the collection. In addition, you may use the `has` method to scope a chain of assertions:\n\n```php\n$response\n    ->assertJson(fn (AssertableJson $json) =>\n        $json->has('meta')\n            ->has('users', 3)\n            ->has('users.0', fn (AssertableJson $json) =>\n                $json->where('id', 1)\n                    ->where('name', 'Victoria Faith')\n                    ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))\n                    ->missing('password')\n                    ->etc()\n            )\n    );\n```\n\nHowever, instead of making two separate calls to the `has` method to assert against the `users` collection, you may make a single call which provides a closure as its third parameter. When doing so, the closure will automatically be invoked and scoped to the first item in the collection:\n\n```php\n$response\n    ->assertJson(fn (AssertableJson $json) =>\n        $json->has('meta')\n            ->has('users', 3, fn (AssertableJson $json) =>\n                $json->where('id', 1)\n                    ->where('name', 'Victoria Faith')\n                    ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))\n                    ->missing('password')\n                    ->etc()\n            )\n    );\n```\n\n---\n\n## Stress Testing The Stress function Requests Failed Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Failed Rate\n\nReturns the number of requests that failed per second.\n\n```php\n$result->requests()->failed()->rate();\n```\n\n---\n\n## Http Tests Testing JSON APIs Asserting Exact JSON Matches\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### Asserting Exact JSON Matches\n\nAs previously mentioned, the `assertJson` method may be used to assert that a fragment of JSON exists within the JSON response. If you would like to verify that a given array **exactly matches** the JSON returned by your application, you should use the `assertExactJson` method:\n\n```php tab=Pest\n<?php\n\ntest('asserting an exact json match', function () {\n    $response = $this->postJson('/user', ['name' => 'Sally']);\n\n    $response\n        ->assertStatus(201)\n        ->assertExactJson([\n            'created' => true,\n        ]);\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    /**\n     * A basic functional test example.\n     */\n    public function test_asserting_an_exact_json_match(): void\n    {\n        $response = $this->postJson('/user', ['name' => 'Sally']);\n\n        $response\n            ->assertStatus(201)\n            ->assertExactJson([\n                'created' => true,\n            ]);\n    }\n}\n```\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Defining Rate Limiters\n\nLaravel includes powerful and customizable rate limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs.\n\nRate limiters may be defined within the `boot` method of your application's `App\\Providers\\AppServiceProvider` class:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\n/**\n * Bootstrap any application services.\n */\npublic function boot(): void\n{\n    RateLimiter::for('api', function (Request $request) {\n        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());\n    });\n}\n```\n\nRate limiters are defined using the `RateLimiter` facade's `for` method. The `for` method accepts a rate limiter name and a closure that returns the limit configuration that should apply to routes that are assigned to the rate limiter. Limit configuration are instances of the `Illuminate\\Cache\\RateLimiting\\Limit` class. This class contains helpful \"builder\" methods so that you can quickly define your limit. The rate limiter name may be any string you wish:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\n/**\n * Bootstrap any application services.\n */\npublic function boot(): void\n{\n    RateLimiter::for('global', function (Request $request) {\n        return Limit::perMinute(1000);\n    });\n}\n```\n\nIf the incoming request exceeds the specified rate limit, a response with a 429 HTTP status code will automatically be returned by Laravel. If you would like to define your own response that should be returned by a rate limit, you may use the `response` method:\n\n```php\nRateLimiter::for('global', function (Request $request) {\n    return Limit::perMinute(1000)->response(function (Request $request, array $headers) {\n        return response('Custom response...', 429, $headers);\n    });\n});\n```\n\nSince rate limiter callbacks receive the incoming HTTP request instance, you may build the appropriate rate limit dynamically based on the incoming request or authenticated user:\n\n```php\nRateLimiter::for('uploads', function (Request $request) {\n    return $request->user()->vipCustomer()\n        ? Limit::none()\n        : Limit::perHour(10);\n});\n```\n\n---\n\n## Http Tests Testing JSON APIs Fluent JSON Testing Asserting JSON Types\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### Asserting JSON Types\n\nYou may only want to assert that the properties in the JSON response are of a certain type. The `Illuminate\\Testing\\Fluent\\AssertableJson` class provides the `whereType` and `whereAllType` methods for doing just that:\n\n```php\n$response->assertJson(fn (AssertableJson $json) =>\n    $json->whereType('id', 'integer')\n        ->whereAllType([\n            'users.0.name' => 'string',\n            'meta' => 'array'\n        ])\n);\n```\n\nYou may specify multiple types using the `|` character, or passing an array of types as the second parameter to the `whereType` method. The assertion will be successful if the response value is any of the listed types:\n\n```php\n$response->assertJson(fn (AssertableJson $json) =>\n    $json->whereType('name', 'string|null')\n        ->whereType('id', ['string', 'integer'])\n);\n```\n\nThe `whereType` and `whereAllType` methods recognize the following types: `string`, `integer`, `double`, `boolean`, `array`, and `null`.\n\n---\n\n## Stress Testing The Stress function Requests Rate\nStress Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/stress-testing.md\n\n\n### Requests Rate\n\nReturns the number of requests made per second.\n\n```php\n$result->requests()->rate();\n```\n\n---\n\n## Strings Fluent Strings `isJson` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `isJson` {.collection-method}\n\nThe `isJson` method determines if a given string is valid JSON:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$result = Str::of('[1,2,3]')->isJson();\n\n// true\n\n$result = Str::of('{\"first\": \"John\", \"last\": \"Doe\"}')->isJson();\n\n// true\n\n$result = Str::of('{first: \"John\", last: \"Doe\"}')->isJson();\n\n// false\n```\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters Multiple Rate Limits\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n#### Multiple Rate Limits\n\nIf needed, you may return an array of rate limits for a given rate limiter configuration. Each rate limit will be evaluated for the route based on the order they are placed within the array:\n\n```php\nRateLimiter::for('login', function (Request $request) {\n    return [\n        Limit::perMinute(500),\n        Limit::perMinute(3)->by($request->input('email')),\n    ];\n});\n```\n\nIf you're assigning multiple rate limits segmented by identical `by` values, you should ensure that each `by` value is unique. The easiest way to achieve this is to prefix the values given to the `by` method:\n\n```php\nRateLimiter::for('uploads', function (Request $request) {\n    return [\n        Limit::perMinute(10)->by('minute:'.$request->user()->id),\n        Limit::perDay(1000)->by('day:'.$request->user()->id),\n    ];\n});\n```\n\n---\n\n## Strings Fluent Strings `transliterate` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `transliterate` {.collection-method}\n\nThe `transliterate` method will attempt to convert a given string into its closest ASCII representation:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$email = Str::of('ⓣⓔⓢⓣ@ⓛⓐⓡⓐⓥⓔⓛ.ⓒⓞⓜ')->transliterate()\n\n// 'test@laravel.com'\n```\n\n---\n\n## Strings Strings `Str::isJson()` {.collection-method}\nStrings | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/strings.md\n\n\n#### `Str::isJson()` {.collection-method}\n\nThe `Str::isJson` method determines if the given string is valid JSON:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$result = Str::isJson('[1,2,3]');\n\n// true\n\n$result = Str::isJson('{\"first\": \"John\", \"last\": \"Doe\"}');\n\n// true\n\n$result = Str::isJson('{first: \"John\", last: \"Doe\"}');\n\n// false\n```\n\n---\n\n## Routing Rate Limiting Attaching Rate Limiters to Routes Throttling With Redis\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n#### Throttling With Redis\n\nBy default, the `throttle` middleware is mapped to the `Illuminate\\Routing\\Middleware\\ThrottleRequests` class. However, if you are using Redis as your application's cache driver, you may wish to instruct Laravel to use Redis to manage rate limiting. To do so, you should use the `throttleWithRedis` method in your application's `bootstrap/app.php` file. This method maps the `throttle` middleware to the `Illuminate\\Routing\\Middleware\\ThrottleRequestsWithRedis` middleware class:\n\n```php\n->withMiddleware(function (Middleware $middleware): void {\n    $middleware->throttleWithRedis();\n    // ...\n})\n```\n\n---\n\n## Http Tests Testing JSON APIs Fluent JSON Testing Asserting Attribute Presence / Absence\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### Asserting Attribute Presence / Absence\n\nTo assert that an attribute is present or absent, you may use the `has` and `missing` methods:\n\n```php\n$response->assertJson(fn (AssertableJson $json) =>\n    $json->has('data')\n        ->missing('message')\n);\n```\n\nIn addition, the `hasAll` and `missingAll` methods allow asserting the presence or absence of multiple attributes simultaneously:\n\n```php\n$response->assertJson(fn (AssertableJson $json) =>\n    $json->hasAll(['status', 'data'])\n        ->missingAll(['message', 'code'])\n);\n```\n\nYou may use the `hasAny` method to determine if at least one of a given list of attributes is present:\n\n```php\n$response->assertJson(fn (AssertableJson $json) =>\n    $json->has('status')\n        ->hasAny('data', 'message', 'code')\n);\n```\n\n---\n\n## Responses Streamed Responses Streamed JSON Responses\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### Streamed JSON Responses\n\nIf you need to stream JSON data incrementally, you may utilize the `streamJson` method. This method is especially useful for large datasets that need to be sent progressively to the browser in a format that can be easily parsed by JavaScript:\n\n```php\nuse App\\Models\\User;\n\nRoute::get('/users.json', function () {\n    return response()->streamJson([\n        'users' => User::cursor(),\n    ]);\n});\n```\n\nThe `useJsonStream` hook is identical to the [useStream hook](#consuming-streamed-responses) except that it will attempt to parse the data as JSON once it has finished streaming:\n\n```tsx tab=React\nimport { useJsonStream } from \"@laravel/stream-react\";\n\ntype User = {\n    id: number;\n    name: string;\n    email: string;\n};\n\nfunction App() {\n    const { data, send } = useJsonStream<{ users: User[] }>(\"users\");\n\n    const loadUsers = () => {\n        send({\n            query: \"taylor\",\n        });\n    };\n\n    return (\n        <div>\n            <ul>\n                {data?.users.map((user) => (\n                    <li>\n                        {user.id}: {user.name}\n                    </li>\n                ))}\n            </ul>\n            <button onClick={loadUsers}>Load Users</button>\n        </div>\n    );\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useJsonStream } from \"@laravel/stream-vue\";\n\ntype User = {\n    id: number;\n    name: string;\n    email: string;\n};\n\nconst { data, send } = useJsonStream<{ users: User[] }>(\"users\");\n\nconst loadUsers = () => {\n    send({\n        query: \"taylor\",\n    });\n};\n</script>\n\n<template>\n    <div>\n        <ul>\n            <li v-for=\"user in data?.users\" :key=\"user.id\">\n                {{ user.id }}: {{ user.name }}\n            </li>\n        </ul>\n        <button @click=\"loadUsers\">Load Users</button>\n    </div>\n</template>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useJsonStream } from \"@laravel/stream-svelte\";\n\nconst stream = useJsonStream(\"users\");\n\nconst loadUsers = () => {\n    stream.send({\n        query: \"taylor\",\n    });\n};\n</script>\n\n<div>\n    <ul>\n        {#if $stream.data?.users}\n            {#each $stream.data.users as user (user.id)}\n                <li>{user.id}: {user.name}</li>\n            {/each}\n        {/if}\n    </ul>\n    <button onclick={loadUsers}>Load Users</button>\n</div>\n```\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters Response-Based Rate Limiting\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n#### Response-Based Rate Limiting\n\nIn addition to rate limiting incoming requests, Laravel allows you to rate limit based on the response using the `after` method. This is useful when you only want to count certain responses toward the rate limit, such as validation errors, 404 responses, or other specific HTTP status codes.\n\nThe `after` method accepts a closure that receives the response and should return `true` if the response should be counted toward the rate limit, or `false` if it should be ignored. This is particularly useful for preventing enumeration attacks by limiting consecutive 404 responses, or allowing users to retry requests that fail validation without exhausting their rate limit on an endpoint that should only throttle successful operations:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nRateLimiter::for('resource-not-found', function (Request $request) {\n    return Limit::perMinute(10)\n        ->by($request->user()?->id ?: $request->ip())\n        ->after(function (Response $response) {\n            // Only count 404 responses toward the rate limit to prevent enumeration...\n            return $response->status() === 404;\n        });\n});\n```\n\n_[Truncated]_"}]


find app/Http/Controllers/Api app/Http/Requests/Api app/Http/Resources -type f | sort


sed -n '1,280p' app/Http/Controllers/Api/Storefront/V1/CheckoutController.php


sed -n '1,240p' app/Http/Resources/Storefront/V1/CheckoutResource.php


/bin/zsh
-lc
sed -n '1,260p' routes/api.php

<?php

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


sed -n '1,240p' app/Http/Resources/Storefront/V1/CheckoutResource.php

<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CheckoutResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'cart_id' => $this->cart_id,
            'customer_id' => $this->customer_id,
            'status' => $this->status?->value,
            'payment_method' => $this->payment_method,
            'email' => $this->email,
            'shipping_address' => $this->shipping_address_json,
            'billing_address' => $this->billing_address_json,
            'shipping_method_id' => $this->shipping_method_id,
            'discount_code' => $this->discount_code,
            'totals' => $this->totals_json,
            'tax_provider_snapshot' => $this->tax_provider_snapshot_json,
            'available_shipping_rates' => ShippingRateResource::collection($this->whenLoaded('availableRates')),
            'cart' => new CartResource($this->whenLoaded('cart')),
            'expires_at' => $this->expires_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


sed -n '1,280p' app/Http/Controllers/Api/Storefront/V1/CheckoutController.php

<?php

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

use App\Exceptions\InsufficientInventoryException;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\ApplyCheckoutDiscountRequest;
use App\Http\Requests\Api\Storefront\V1\SelectCheckoutPaymentRequest;
use App\Http\Requests\Api\Storefront\V1\SetCheckoutAddressRequest;
use App\Http\Requests\Api\Storefront\V1\SetCheckoutShippingRequest;
use App\Http\Requests\Api\Storefront\V1\StoreCheckoutRequest;
use App\Http\Resources\Storefront\V1\CheckoutResource;
use App\Models\Cart;
use App\Models\Checkout;
use App\Models\ShippingRate;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;

class CheckoutController extends Controller
{
    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts, PricingEngine $pricing): CheckoutResource|JsonResponse
    {
        $cart = Cart::query()->findOrFail($request->validated('cart_id'));

        try {
            $checkout = $checkouts->createFromCart($cart);
            $checkout->forceFill([
                'email' => (string) $request->validated('email'),
            ])->save();
            $pricing->calculate($checkout);

            return CheckoutResource::make($this->loadCheckout(
                $checkout->refresh(),
            ))->response()->setStatusCode(201);
        } catch (InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function show(Checkout $checkout): CheckoutResource
    {
        return CheckoutResource::make($this->loadCheckout($checkout));
    }

    public function address(SetCheckoutAddressRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        $addressData = $request->validated();
        $addressData['email'] ??= $checkout->email;

        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->setAddress($checkout, $addressData)));
        } catch (InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function shippingMethod(SetCheckoutShippingRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->setShippingMethod(
                $checkout,
                $request->validated('shipping_rate_id'),
            )));
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function applyDiscount(ApplyCheckoutDiscountRequest $request, Checkout $checkout, PricingEngine $pricing): CheckoutResource|JsonResponse
    {
        $checkout->forceFill([
            'discount_code' => trim((string) $request->validated('code')) ?: null,
        ])->save();

        try {
            $pricing->calculate($checkout);

            return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
        } catch (InvalidDiscountException $exception) {
            $checkout->forceFill(['discount_code' => null])->save();
            $pricing->calculate($checkout);

            return response()->json([
                'message' => $exception->getMessage(),
                'reason' => $exception->reasonCode,
            ], 422);
        }
    }

    public function destroyDiscount(Checkout $checkout, PricingEngine $pricing): CheckoutResource
    {
        abort_if($checkout->discount_code === null, 404);

        $checkout->forceFill(['discount_code' => null])->save();
        $pricing->calculate($checkout);

        return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
    }

    public function paymentMethod(SelectCheckoutPaymentRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->selectPaymentMethod(
                $checkout,
                (string) $request->validated('payment_method'),
            )));
        } catch (InsufficientInventoryException|InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    private function loadCheckout(Checkout $checkout): Checkout
    {
        $checkout = $checkout->load([
            'cart.lines.variant.product',
            'cart.lines.variant.optionValues.option',
            'store',
        ]);

        $checkout->setRelation('availableRates', $this->availableRates($checkout));

        return $checkout;
    }

    /**
     * @return Collection<int, ShippingRate>
     */
    private function availableRates(Checkout $checkout): Collection
    {
        if (! is_array($checkout->shipping_address_json) || $checkout->shipping_address_json === []) {
            return collect();
        }

        return app(ShippingCalculator::class)
            ->getAvailableRates($checkout->store, $checkout->shipping_address_json)
            ->map(function (ShippingRate $rate) use ($checkout): ShippingRate {
                $rate->setAttribute('calculated_amount', app(ShippingCalculator::class)->calculate($rate, $checkout->cart));

                return $rate;
            });
    }
}


Output:
Chunk ID: b05778
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 244
Output:
app/Http/Controllers/Api/Storefront/V1/CartController.php
app/Http/Controllers/Api/Storefront/V1/CartLineController.php
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php
app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/SelectCheckoutPaymentRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutShippingRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php
app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php
app/Http/Resources/Storefront/V1/CartLineResource.php
app/Http/Resources/Storefront/V1/CartResource.php
app/Http/Resources/Storefront/V1/CheckoutResource.php
app/Http/Resources/Storefront/V1/ShippingRateResource.php


Output:
Chunk ID: 550fcf
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 502
Output:
<?php

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


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

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

use App\Exceptions\InsufficientInventoryException;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\ApplyCheckoutDiscountRequest;
use App\Http\Requests\Api\Storefront\V1\SelectCheckoutPaymentRequest;
use App\Http\Requests\Api\Storefront\V1\SetCheckoutAddressRequest;
use App\Http\Requests\Api\Storefront\V1\SetCheckoutShippingRequest;
use App\Http\Requests\Api\Storefront\V1\StoreCheckoutRequest;
use App\Http\Resources\Storefront\V1\CheckoutResource;
use App\Models\Cart;
use App\Models\Checkout;
use App\Models\ShippingRate;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;

class CheckoutController extends Controller
{
    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts, PricingEngine $pricing): CheckoutResource|JsonResponse
    {
        $cart = Cart::query()->findOrFail($request->validated('cart_id'));

        try {
            $checkout = $checkouts->createFromCart($cart);
            $checkout->forceFill([
                'email' => (string) $request->validated('email'),
            ])->save();
            $pricing->calculate($checkout);

            return CheckoutResource::make($this->loadCheckout(
                $checkout->refresh(),
            ))->response()->setStatusCode(201);
        } catch (InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function show(Checkout $checkout): CheckoutResource
    {
        return CheckoutResource::make($this->loadCheckout($checkout));
    }

    public function address(SetCheckoutAddressRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        $addressData = $request->validated();
        $addressData['email'] ??= $checkout->email;

        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->setAddress($checkout, $addressData)));
        } catch (InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function shippingMethod(SetCheckoutShippingRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->setShippingMethod(
                $checkout,
                $request->validated('shipping_rate_id'),
            )));
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function applyDiscount(ApplyCheckoutDiscountRequest $request, Checkout $checkout, PricingEngine $pricing): CheckoutResource|JsonResponse
    {
        $checkout->forceFill([
            'discount_code' => trim((string) $request->validated('code')) ?: null,
        ])->save();

        try {
            $pricing->calculate($checkout);

            return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
        } catch (InvalidDiscountException $exception) {
            $checkout->forceFill(['discount_code' => null])->save();
            $pricing->calculate($checkout);

            return response()->json([
                'message' => $exception->getMessage(),
                'reason' => $exception->reasonCode,
            ], 422);
        }
    }

    public function destroyDiscount(Checkout $checkout, PricingEngine $pricing): CheckoutResource
    {
        abort_if($checkout->discount_code === null, 404);

        $checkout->forceFill(['discount_code' => null])->save();
        $pricing->calculate($checkout);

        return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
    }

    public function paymentMethod(SelectCheckoutPaymentRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
    {
        try {
            return CheckoutResource::make($this->loadCheckout($checkouts->selectPaymentMethod(
                $checkout,
                (string) $request->validated('payment_method'),
            )));
        } catch (InsufficientInventoryException|InvalidCheckoutTransitionException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    private function loadCheckout(Checkout $checkout): Checkout
    {
        $checkout = $checkout->load([
            'cart.lines.variant.product',
            'cart.lines.variant.optionValues.option',
            'store',
        ]);

        $checkout->setRelation('availableRates', $this->availableRates($checkout));

        return $checkout;
    }

    /**
     * @return Collection<int, ShippingRate>
     */
    private function availableRates(Checkout $checkout): Collection
    {
        if (! is_array($checkout->shipping_address_json) || $checkout->shipping_address_json === []) {
            return collect();
        }

        return app(ShippingCalculator::class)
            ->getAvailableRates($checkout->store, $checkout->shipping_address_json)
            ->map(function (ShippingRate $rate) use ($checkout): ShippingRate {
                $rate->setAttribute('calculated_amount', app(ShippingCalculator::class)->calculate($rate, $checkout->cart));

                return $rate;
            });
    }
}


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

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CheckoutResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'cart_id' => $this->cart_id,
            'customer_id' => $this->customer_id,
            'status' => $this->status?->value,
            'payment_method' => $this->payment_method,
            'email' => $this->email,
            'shipping_address' => $this->shipping_address_json,
            'billing_address' => $this->billing_address_json,
            'shipping_method_id' => $this->shipping_method_id,
            'discount_code' => $this->discount_code,
            'totals' => $this->totals_json,
            'tax_provider_snapshot' => $this->tax_provider_snapshot_json,
            'available_shipping_rates' => ShippingRateResource::collection($this->whenLoaded('availableRates')),
            'cart' => new CartResource($this->whenLoaded('cart')),
            'expires_at' => $this->expires_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


sed -n '1,260p' app/Http/Controllers/Api/Storefront/V1/CartController.php && sed -n '1,260p' app/Http/Controllers/Api/Storefront/V1/CartLineController.php


sed -n '1,180p' app/Http/Requests/Api/Storefront/V1/SelectCheckoutPaymentRequest.php && sed -n '1,180p' app/Http/Requests/Api/Storefront/V1/SetCheckoutShippingRequest.php


sed -n '1,280p' tests/Feature/Api/StorefrontCheckoutApiTest.php


sed -n '1,180p' app/Http/Requests/Api/Storefront/V1/SelectCheckoutPaymentRequest.php && sed -n '1,180p' app/Http/Requests/Api/Storefront/V1/SetCheckoutShippingRequest.php

SetCheckoutShippingRequest.php

<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'payment_method' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ];
    }
}
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'shipping_rate_id' => ['nullable', 'integer', 'exists:shipping_rates,id'],
        ];
    }
}


sed -n '1,260p' app/Http/Controllers/Api/Storefront/V1/CartController.php && sed -n '1,260p' app/Http/Controllers/Api/Storefront/V1/CartLineController.php

CartLineController.php

<?php

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

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartController extends Controller
{
    public function store(StoreCartRequest $request, CartService $carts): JsonResponse
    {
        $request->validated();

        return CartResource::make($this->loadCart($carts->create($this->currentStore())))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Cart $cart): CartResource
    {
        return CartResource::make($this->loadCart($cart));
    }

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

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}
<?php

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

use App\Exceptions\CartVersionMismatchException;
use App\Exceptions\InsufficientInventoryException;
use App\Exceptions\InvalidCartOperationException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\DestroyCartLineRequest;
use App\Http\Requests\Api\Storefront\V1\StoreCartLineRequest;
use App\Http\Requests\Api\Storefront\V1\UpdateCartLineRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\CartLine;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartLineController extends Controller
{
    public function store(StoreCartLineRequest $request, Cart $cart, CartService $carts): CartResource|JsonResponse
    {
        try {
            $carts->addLine(
                $cart,
                (int) $request->validated('variant_id'),
                (int) $request->validated('quantity'),
                $request->expectedVersion(),
            );

            return CartResource::make($this->loadCart($cart->refresh()))
                ->response()
                ->setStatusCode(201);
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InsufficientInventoryException|InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function update(UpdateCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
    {
        abort_unless((int) $cartLine->cart_id === (int) $cart->getKey(), 404);

        try {
            $carts->updateLineQuantity(
                $cart,
                $cartLine->getKey(),
                (int) $request->validated('quantity'),
                $request->expectedVersion(),
            );

            return CartResource::make($this->loadCart($cart->refresh()));
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InsufficientInventoryException|InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function destroy(DestroyCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
    {
        abort_unless((int) $cartLine->cart_id === (int) $cart->getKey(), 404);

        try {
            $carts->removeLine($cart, $cartLine->getKey(), $request->expectedVersion());
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }

        return CartResource::make($this->loadCart($cart->refresh()));
    }

    private function versionMismatch(CartVersionMismatchException $exception, Cart $cart): JsonResponse
    {
        return response()->json([
            'message' => $exception->getMessage(),
            'expected_cart_version' => $exception->expectedVersion,
            'current_cart_version' => $exception->currentVersion,
            'cart' => CartResource::make($this->loadCart($cart->refresh())),
        ], 409);
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}


Output:
Chunk ID: 88ff79
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1180
Output:
<?php

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

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\StoreCartRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartController extends Controller
{
    public function store(StoreCartRequest $request, CartService $carts): JsonResponse
    {
        $request->validated();

        return CartResource::make($this->loadCart($carts->create($this->currentStore())))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Cart $cart): CartResource
    {
        return CartResource::make($this->loadCart($cart));
    }

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

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}
<?php

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

use App\Exceptions\CartVersionMismatchException;
use App\Exceptions\InsufficientInventoryException;
use App\Exceptions\InvalidCartOperationException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Storefront\V1\DestroyCartLineRequest;
use App\Http\Requests\Api\Storefront\V1\StoreCartLineRequest;
use App\Http\Requests\Api\Storefront\V1\UpdateCartLineRequest;
use App\Http\Resources\Storefront\V1\CartResource;
use App\Models\Cart;
use App\Models\CartLine;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;

class CartLineController extends Controller
{
    public function store(StoreCartLineRequest $request, Cart $cart, CartService $carts): CartResource|JsonResponse
    {
        try {
            $carts->addLine(
                $cart,
                (int) $request->validated('variant_id'),
                (int) $request->validated('quantity'),
                $request->expectedVersion(),
            );

            return CartResource::make($this->loadCart($cart->refresh()))
                ->response()
                ->setStatusCode(201);
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InsufficientInventoryException|InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function update(UpdateCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
    {
        abort_unless((int) $cartLine->cart_id === (int) $cart->getKey(), 404);

        try {
            $carts->updateLineQuantity(
                $cart,
                $cartLine->getKey(),
                (int) $request->validated('quantity'),
                $request->expectedVersion(),
            );

            return CartResource::make($this->loadCart($cart->refresh()));
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InsufficientInventoryException|InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }
    }

    public function destroy(DestroyCartLineRequest $request, Cart $cart, CartLine $cartLine, CartService $carts): CartResource|JsonResponse
    {
        abort_unless((int) $cartLine->cart_id === (int) $cart->getKey(), 404);

        try {
            $carts->removeLine($cart, $cartLine->getKey(), $request->expectedVersion());
        } catch (CartVersionMismatchException $exception) {
            return $this->versionMismatch($exception, $cart);
        } catch (InvalidCartOperationException $exception) {
            return response()->json(['message' => $exception->getMessage()], 422);
        }

        return CartResource::make($this->loadCart($cart->refresh()));
    }

    private function versionMismatch(CartVersionMismatchException $exception, Cart $cart): JsonResponse
    {
        return response()->json([
            'message' => $exception->getMessage(),
            'expected_cart_version' => $exception->expectedVersion,
            'current_cart_version' => $exception->currentVersion,
            'cart' => CartResource::make($this->loadCart($cart->refresh())),
        ], 409);
    }

    private function loadCart(Cart $cart): Cart
    {
        return $cart->load([
            'lines.variant.product',
            'lines.variant.optionValues.option',
        ]);
    }
}


Output:
Chunk ID: 5285b9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 326
Output:
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'payment_method' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ];
    }
}
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'shipping_rate_id' => ['nullable', 'integer', 'exists:shipping_rates,id'],
        ];
    }
}


sed -n '1,280p' tests/Feature/Api/StorefrontCheckoutApiTest.php

<?php

use App\Enums\CheckoutStatus;
use App\Models\Checkout;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

function storefrontApiCheckoutVariant(Store $store): ProductVariant
{
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'classic-cotton-t-shirt')
        ->firstOrFail();

    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->oldest('position')
        ->firstOrFail();

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

    return $variant->refresh();
}

/**
 * @return array<string, string>
 */
function storefrontApiCheckoutAddress(string $country = 'DE'): array
{
    return [
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Main Street 1',
        'city' => 'Berlin',
        'province_code' => 'BE',
        'country' => $country,
        'country_code' => $country,
        'postal_code' => '10115',
    ];
}

test('storefront checkout api progresses through address shipping discount removal and payment selection', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.21'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 2,
        ])
        ->assertCreated();

    $checkoutResponse = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ]);

    $checkoutResponse
        ->assertCreated()
        ->assertJsonPath('data.status', 'started')
        ->assertJsonPath('data.email', 'buyer@example.test')
        ->assertJsonPath('data.totals.subtotal', 4998)
        ->assertJsonPath('data.cart.line_count', 2);

    $checkoutId = $checkoutResponse['data']['id'];

    $addressResponse = $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ]);

    $addressResponse
        ->assertOk()
        ->assertJsonPath('data.status', 'addressed')
        ->assertJsonPath('data.shipping_address.country', 'DE')
        ->assertJsonCount(3, 'data.available_shipping_rates');

    $shippingRateId = $addressResponse['data']['available_shipping_rates'][0]['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $shippingRateId,
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'shipping_selected')
        ->assertJsonPath('data.shipping_method_id', $shippingRateId)
        ->assertJsonPath('data.totals.shipping', 799);

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'SAVE10'])
        ->assertOk()
        ->assertJsonPath('data.discount_code', 'SAVE10')
        ->assertJsonPath('data.totals.discount', 500);

    $api()
        ->deleteJson("/api/storefront/v1/checkouts/{$checkoutId}/discount")
        ->assertOk()
        ->assertJsonPath('data.discount_code', null)
        ->assertJsonPath('data.totals.discount', 0);

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/payment-method", [
            'payment_method' => 'credit_card',
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'payment_selected')
        ->assertJsonPath('data.payment_method', 'credit_card');

    $checkout = Checkout::withoutGlobalScopes()->findOrFail($checkoutId);
    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($inventory->quantity_reserved)->toBe(2);
});

test('storefront checkout api rejects invalid addresses shipping methods and discounts', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.22'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 1,
        ])
        ->assertCreated();

    $checkoutId = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ])
        ->assertCreated()['data']['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => array_diff_key(storefrontApiCheckoutAddress(), ['first_name' => true]),
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors('shipping_address.first_name');

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ])
        ->assertOk();

    $otherStoreRate = ShippingRate::withoutGlobalScopes()
        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()))
        ->firstOrFail();

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $otherStoreRate->getKey(),
        ])
        ->assertUnprocessable();

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'NOTREAL'])
        ->assertUnprocessable()
        ->assertJsonPath('reason', 'discount_not_found');
});


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

use App\Enums\CheckoutStatus;
use App\Models\Checkout;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

function storefrontApiCheckoutVariant(Store $store): ProductVariant
{
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'classic-cotton-t-shirt')
        ->firstOrFail();

    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->oldest('position')
        ->firstOrFail();

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

    return $variant->refresh();
}

/**
 * @return array<string, string>
 */
function storefrontApiCheckoutAddress(string $country = 'DE'): array
{
    return [
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Main Street 1',
        'city' => 'Berlin',
        'province_code' => 'BE',
        'country' => $country,
        'country_code' => $country,
        'postal_code' => '10115',
    ];
}

test('storefront checkout api progresses through address shipping discount removal and payment selection', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.21'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 2,
        ])
        ->assertCreated();

    $checkoutResponse = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ]);

    $checkoutResponse
        ->assertCreated()
        ->assertJsonPath('data.status', 'started')
        ->assertJsonPath('data.email', 'buyer@example.test')
        ->assertJsonPath('data.totals.subtotal', 4998)
        ->assertJsonPath('data.cart.line_count', 2);

    $checkoutId = $checkoutResponse['data']['id'];

    $addressResponse = $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ]);

    $addressResponse
        ->assertOk()
        ->assertJsonPath('data.status', 'addressed')
        ->assertJsonPath('data.shipping_address.country', 'DE')
        ->assertJsonCount(3, 'data.available_shipping_rates');

    $shippingRateId = $addressResponse['data']['available_shipping_rates'][0]['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $shippingRateId,
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'shipping_selected')
        ->assertJsonPath('data.shipping_method_id', $shippingRateId)
        ->assertJsonPath('data.totals.shipping', 799);

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'SAVE10'])
        ->assertOk()
        ->assertJsonPath('data.discount_code', 'SAVE10')
        ->assertJsonPath('data.totals.discount', 500);

    $api()
        ->deleteJson("/api/storefront/v1/checkouts/{$checkoutId}/discount")
        ->assertOk()
        ->assertJsonPath('data.discount_code', null)
        ->assertJsonPath('data.totals.discount', 0);

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/payment-method", [
            'payment_method' => 'credit_card',
        ])
        ->assertOk()
        ->assertJsonPath('data.status', 'payment_selected')
        ->assertJsonPath('data.payment_method', 'credit_card');

    $checkout = Checkout::withoutGlobalScopes()->findOrFail($checkoutId);
    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($inventory->quantity_reserved)->toBe(2);
});

test('storefront checkout api rejects invalid addresses shipping methods and discounts', function (): void {
    $store = storefrontApiCheckoutStore();
    $variant = storefrontApiCheckoutVariant($store);
    $api = fn () => $this->withServerVariables(['REMOTE_ADDR' => '10.0.0.22'])->withHeader('Host', 'shop.test');

    $cartId = $api()
        ->postJson('/api/storefront/v1/carts')
        ->assertCreated()['data']['id'];

    $api()
        ->postJson("/api/storefront/v1/carts/{$cartId}/lines", [
            'variant_id' => $variant->getKey(),
            'quantity' => 1,
        ])
        ->assertCreated();

    $checkoutId = $api()
        ->postJson('/api/storefront/v1/checkouts', [
            'cart_id' => $cartId,
            'email' => 'buyer@example.test',
        ])
        ->assertCreated()['data']['id'];

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => array_diff_key(storefrontApiCheckoutAddress(), ['first_name' => true]),
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors('shipping_address.first_name');

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
            'shipping_address' => storefrontApiCheckoutAddress(),
        ])
        ->assertOk();

    $otherStoreRate = ShippingRate::withoutGlobalScopes()
        ->whereHas('zone', fn ($query) => $query->withoutGlobalScopes()->where('store_id', Store::query()->whereKeyNot($store->getKey())->firstOrFail()->getKey()))
        ->firstOrFail();

    $api()
        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
            'shipping_rate_id' => $otherStoreRate->getKey(),
        ])
        ->assertUnprocessable();

    $api()
        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'NOTREAL'])
        ->assertUnprocessable()
        ->assertJsonPath('reason', 'discount_not_found');
});


I’m going to add these as conventional Laravel API controllers/resources/requests. For storefront, `POST /checkouts/{checkout}/pay` will return an order plus an HMAC access token, and `GET /orders/{orderNumber}` will require that token. For admin, I’ll add store-scoped order list/show plus refund and fulfillment write endpoints using the same services as the UI.


php artisan make:controller Api/Storefront/V1/OrderController --no-interaction && php artisan make:controller Api/Admin/V1/OrderController --no-interaction && php artisan make:controller Api/Admin/V1/OrderFulfillmentController --no-interaction && php artisan make:controller Api/Admin/V1/OrderRefundController --no-interaction && php artisan make:request Api/Storefront/V1/CompleteCheckoutPaymentRequest --no-interaction && php artisan make:request Api/Admin/V1/CreateOrderFulfillmentRequest --no-interaction && php artisan make:request Api/Admin/V1/CreateOrderRefundRequest --no-interaction && php artisan make:resource Storefront/V1/OrderResource --no-interaction && php artisan make:resource Admin/V1/OrderResource --no-interaction && php artisan make:resource Admin/V1/FulfillmentResource --no-interaction && php artisan make:resource Admin/V1/RefundResource --no-interaction && php artisan make:class Support/OrderAccessToken --no-interaction && php artisan make:test --pest Api/StorefrontOrderApiTest --no-interaction && php artisan make:test --pest Api/AdminOrderApiTest --no-interaction


Output:
Chunk ID: 95b945
Wall time: 1.0005 seconds
Process running with session ID 49144
Original token count: 191
Output:

   INFO  Controller [app/Http/Controllers/Api/Storefront/V1/OrderController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderRefundController.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php] created successfully.


php artisan make:controller Api/Storefront/V1/OrderController --no-interaction && php artisan make:controller Api/Admin/V1/OrderController --no-interaction && php artisan make:controller Api/Admin/V1/OrderFulfillmentController --no-interaction && php artisan make:controller Api/Admin/V1/OrderRefundController --no-interaction && php artisan make:request Api/Storefront/V1/CompleteCheckoutPaymentRequest --no-interaction && php artisan make:request Api/Admin/V1/CreateOrderFulfillmentRequest --no-interaction && php artisan make:request Api/Admin/V1/CreateOrderRefundRequest --no-interaction && php artisan make:resource Storefront/V1/OrderResource --no-interaction && php artisan make:resource Admin/V1/OrderResource --no-interaction && php artisan make:resource Admin/V1/FulfillmentResource --no-interaction && php artisan make:resource Admin/V1/RefundResource --no-interaction && php artisan make:class Support/OrderAccessToken --no-interaction && php artisan make:test --pest Api/StorefrontOrderApiTest --no-interaction && php artisan make:test --pest Api/AdminOrderApiTest --no-interaction

   INFO  Controller [app/Http/Controllers/Api/Storefront/V1/OrderController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php] created successfully.

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/OrderRefundController.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php] created successfully.

   INFO  Request [app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php] created successfully.

   INFO  Resource [app/Http/Resources/Storefront/V1/OrderResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/OrderResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/FulfillmentResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/RefundResource.php] created successfully.

   INFO  Class [app/Support/OrderAccessToken.php] created successfully.

   INFO  Test [tests/Feature/Api/StorefrontOrderApiTest.php] created successfully.

   INFO  Test [tests/Feature/Api/AdminOrderApiTest.php] created successfully.


Output:
Chunk ID: 5f2ff8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 157
Output:

   INFO  Resource [app/Http/Resources/Storefront/V1/OrderResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/OrderResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/FulfillmentResource.php] created successfully.

   INFO  Resource [app/Http/Resources/Admin/V1/RefundResource.php] created successfully.

   INFO  Class [app/Support/OrderAccessToken.php] created successfully.

   INFO  Test [tests/Feature/Api/StorefrontOrderApiTest.php] created successfully.

   INFO  Test [tests/Feature/Api/AdminOrderApiTest.php] created successfully.


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Support/OrderAccessToken.php
@@
 namespace App\Support;

+use App\Models\Order;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Str;
+
 class OrderAccessToken
 {
+    public static function make(Order $order): string
+    {
+        return hash_hmac('sha256', self::payload($order), self::key());
+    }
+
+    public static function valid(Order $order, ?string $token): bool
+    {
+        if ($token === null || $token === '') {
+            return false;
+        }
+
+        return hash_equals(self::make($order), $token);
+    }
+
+    private static function payload(Order $order): string
+    {
+        return implode('|', [
+            $order->store_id,
+            $order->getKey(),
+            $order->order_number,
+            $order->created_at?->timestamp ?? 0,
+        ]);
+    }
+
+    private static function key(): string
+    {
+        $key = (string) Config::get('app.key');
+
+        return Str::startsWith($key, 'base64:')
+            ? base64_decode(Str::after($key, 'base64:'), true) ?: $key
+            : $key;
+    }
 }
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/app/Support/OrderAccessToken.php:
namespace App\Support;

class OrderAccessToken
{
}


sed -n '1,80p' app/Support/OrderAccessToken.php


sed -n '1,80p' app/Support/OrderAccessToken.php

<?php

namespace App\Support;

class OrderAccessToken
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


Output:
Chunk ID: 4e0344
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 42
Output:
<?php

namespace App\Support;

class OrderAccessToken
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Support/OrderAccessToken.php
@@
 namespace App\Support;

+use App\Models\Order;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Str;
+
 class OrderAccessToken
 {
-    /**
-     * Create a new class instance.
-     */
-    public function __construct()
+    public static function make(Order $order): string
     {
-        //
+        return hash_hmac('sha256', self::payload($order), self::key());
+    }
+
+    public static function valid(Order $order, ?string $token): bool
+    {
+        if ($token === null || $token === '') {
+            return false;
+        }
+
+        return hash_equals(self::make($order), $token);
+    }
+
+    private static function payload(Order $order): string
+    {
+        return implode('|', [
+            $order->store_id,
+            $order->getKey(),
+            $order->order_number,
+            $order->created_at?->timestamp ?? 0,
+        ]);
+    }
+
+    private static function key(): string
+    {
+        $key = (string) Config::get('app.key');
+
+        return Str::startsWith($key, 'base64:')
+            ? base64_decode(Str::after($key, 'base64:'), true) ?: $key
+            : $key;
     }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Support/OrderAccessToken.php

@@ -4,10 +4,39 @@

+use App\Models\Order;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Str;
+
 class OrderAccessToken
 {
-    /**
-     * Create a new class instance.
-     */
-    public function __construct()
+    public static function make(Order $order): string
+    {
+        return hash_hmac('sha256', self::payload($order), self::key());
+    }
+
+    public static function valid(Order $order, ?string $token): bool
     {
-        //
+        if ($token === null || $token === '') {
+            return false;
+        }
+
+        return hash_equals(self::make($order), $token);
+    }
+
+    private static function payload(Order $order): string
+    {
+        return implode('|', [
+            $order->store_id,
+            $order->getKey(),
+            $order->order_number,
+            $order->created_at?->timestamp ?? 0,
+        ]);
+    }
+
+    private static function key(): string
+    {
+        $key = (string) Config::get('app.key');
+
+        return Str::startsWith($key, 'base64:')
+            ? base64_decode(Str::after($key, 'base64:'), true) ?: $key
+            : $key;
     }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Support/OrderAccessToken.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,160p' app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php


sed -n '1,160p' app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php

<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class CompleteCheckoutPaymentRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php

<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateOrderFulfillmentRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


sed -n '1,160p' app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php

<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateOrderRefundRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


Output:
Chunk ID: 30644e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 147
Output:
<?php

namespace App\Http\Requests\Api\Storefront\V1;

use Illuminate\Foundation\Http\FormRequest;

class CompleteCheckoutPaymentRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


Output:
Chunk ID: 884e11
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 145
Output:
<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateOrderFulfillmentRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


Output:
Chunk ID: 66afd3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 144
Output:
<?php

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

use Illuminate\Foundation\Http\FormRequest;

class CreateOrderRefundRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php
@@
 class CompleteCheckoutPaymentRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'payment_method' => ['required', 'in:credit_card,paypal,bank_transfer'],
+            'card_number' => ['required_if:payment_method,credit_card', 'string'],
+            'card_holder' => ['required_if:payment_method,credit_card', 'string', 'max:255'],
+            'card_expiry' => ['required_if:payment_method,credit_card', 'string', 'max:20'],
+            'card_cvc' => ['required_if:payment_method,credit_card', 'string', 'max:10'],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php

@@ -8,8 +8,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +14,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,3 +19,7 @@
         return [
-            //
+            'payment_method' => ['required', 'in:credit_card,paypal,bank_transfer'],
+            'card_number' => ['required_if:payment_method,credit_card', 'string'],
+            'card_holder' => ['required_if:payment_method,credit_card', 'string', 'max:255'],
+            'card_expiry' => ['required_if:payment_method,credit_card', 'string', 'max:20'],
+            'card_cvc' => ['required_if:payment_method,credit_card', 'string', 'max:10'],
         ];


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
@@
 class CreateOrderFulfillmentRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'line_items' => ['required', 'array', 'min:1'],
+            'line_items.*.order_line_id' => ['required', 'integer'],
+            'line_items.*.quantity' => ['required', 'integer', 'min:1'],
+            'tracking_company' => ['nullable', 'string', 'max:255'],
+            'tracking_number' => ['nullable', 'string', 'max:255'],
+            'tracking_url' => ['nullable', 'url', 'max:2048'],
         ];
     }
+
+    /**
+     * @return array<int, int>
+     */
+    public function lineQuantities(): array
+    {
+        return collect($this->validated('line_items'))
+            ->mapWithKeys(fn (array $line): array => [(int) $line['order_line_id'] => (int) $line['quantity']])
+            ->all();
+    }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php

@@ -8,8 +8,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +14,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,5 +19,20 @@
         return [
-            //
+            'line_items' => ['required', 'array', 'min:1'],
+            'line_items.*.order_line_id' => ['required', 'integer'],
+            'line_items.*.quantity' => ['required', 'integer', 'min:1'],
+            'tracking_company' => ['nullable', 'string', 'max:255'],
+            'tracking_number' => ['nullable', 'string', 'max:255'],
+            'tracking_url' => ['nullable', 'url', 'max:2048'],
         ];
     }
+
+    /**
+     * @return array<int, int>
+     */
+    public function lineQuantities(): array
+    {
+        return collect($this->validated('line_items'))
+            ->mapWithKeys(fn (array $line): array => [(int) $line['order_line_id'] => (int) $line['quantity']])
+            ->all();
+    }
 }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
@@
 class CreateOrderRefundRequest extends FormRequest
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }

     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
      */
     public function rules(): array
     {
         return [
-            //
+            'amount' => ['nullable', 'integer', 'min:1'],
+            'reason' => ['nullable', 'string', 'max:500'],
+            'restock' => ['sometimes', 'boolean'],
+            'line_items' => ['nullable', 'array'],
+            'line_items.*.order_line_id' => ['required_with:line_items', 'integer'],
+            'line_items.*.quantity' => ['required_with:line_items', 'integer', 'min:1'],
         ];
     }
+
+    /**
+     * @return array<int, int>
+     */
+    public function lineQuantities(): array
+    {
+        return collect($this->validated('line_items', []))
+            ->mapWithKeys(fn (array $line): array => [(int) $line['order_line_id'] => (int) $line['quantity']])
+            ->all();
+    }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php

@@ -8,8 +8,5 @@
 {
-    /**
-     * Determine if the user is authorized to make this request.
-     */
     public function authorize(): bool
     {
-        return false;
+        return true;
     }
@@ -17,4 +14,2 @@
     /**
-     * Get the validation rules that apply to the request.
-     *
      * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
@@ -24,5 +19,20 @@
         return [
-            //
+            'amount' => ['nullable', 'integer', 'min:1'],
+            'reason' => ['nullable', 'string', 'max:500'],
+            'restock' => ['sometimes', 'boolean'],
+            'line_items' => ['nullable', 'array'],
+            'line_items.*.order_line_id' => ['required_with:line_items', 'integer'],
+            'line_items.*.quantity' => ['required_with:line_items', 'integer', 'min:1'],
         ];
     }
+
+    /**
+     * @return array<int, int>
+     */
+    public function lineQuantities(): array
+    {
+        return collect($this->validated('line_items', []))
+            ->mapWithKeys(fn (array $line): array => [(int) $line['order_line_id'] => (int) $line['quantity']])
+            ->all();
+    }
 }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Resources/Storefront/V1/OrderResource.php
@@
 namespace App\Http\Resources\Storefront\V1;

+use App\Enums\PaymentMethod;
+use App\Support\OrderAccessToken;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;

 class OrderResource extends JsonResource
 {
@@
     public function toArray(Request $request): array
     {
         return [
-            //
+            'id' => $this->id,
+            'order_number' => $this->order_number,
+            'access_token' => OrderAccessToken::make($this->resource),
+            'email' => $this->email,
+            'status' => $this->status?->value,
+            'financial_status' => $this->financial_status?->value,
+            'fulfillment_status' => $this->fulfillment_status?->value,
+            'payment_method' => $this->payment_method?->value,
+            'currency' => $this->currency,
+            'subtotal_amount' => $this->subtotal_amount,
+            'discount_amount' => $this->discount_amount,
+            'shipping_amount' => $this->shipping_amount,
+            'tax_amount' => $this->tax_amount,
+            'total_amount' => $this->total_amount,
+            'shipping_address' => $this->shipping_address_json,
+            'billing_address' => $this->billing_address_json,
+            'lines' => $this->whenLoaded('lines', fn () => $this->lines->map(fn ($line): array => [
+                'id' => $line->id,
+                'title' => $line->title_snapshot,
+                'sku' => $line->sku_snapshot,
+                'quantity' => $line->quantity,
+                'unit_price_amount' => $line->unit_price_amount,
+                'total_amount' => $line->total_amount,
+            ])->values()),
+            'payments' => $this->whenLoaded('payments', fn () => $this->payments->map(fn ($payment): array => [
+                'id' => $payment->id,
+                'provider' => $payment->provider,
+                'method' => $payment->method?->value,
+                'status' => $payment->status?->value,
+                'amount' => $payment->amount,
+                'currency' => $payment->currency,
+            ])->values()),
+            'fulfillments' => $this->whenLoaded('fulfillments', fn () => $this->fulfillments->map(fn ($fulfillment): array => [
+                'id' => $fulfillment->id,
+                'status' => $fulfillment->status?->value,
+                'tracking_company' => $fulfillment->tracking_company,
+                'tracking_number' => $fulfillment->tracking_number,
+                'tracking_url' => $fulfillment->tracking_url,
+                'shipped_at' => $fulfillment->shipped_at?->toIso8601String(),
+                'delivered_at' => $fulfillment->delivered_at?->toIso8601String(),
+            ])->values()),
+            'bank_transfer_instructions' => $this->payment_method === PaymentMethod::BankTransfer ? [
+                'bank_name' => 'Mock Bank AG',
+                'bic' => 'COBADEFFXXX',
+                'iban' => 'DE89 3704 0044 0532 0130 00',
+                'reference' => $this->order_number,
+                'amount' => $this->total_amount,
+                'currency' => $this->currency,
+            ] : null,
+            'placed_at' => $this->placed_at?->toIso8601String(),
+            'created_at' => $this->created_at?->toIso8601String(),
         ];
     }
 }
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/app/Http/Resources/Storefront/V1/OrderResource.php:
    public function toArray(Request $request): array
    {
        return [
            //
        ];
    }
}


sed -n '1,100p' app/Http/Resources/Storefront/V1/OrderResource.php


sed -n '1,100p' app/Http/Resources/Storefront/V1/OrderResource.php

<?php

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}


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

namespace App\Http\Resources\Storefront\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}