# Search Results

## Validation Available Validation Rules date_format:_format_,...
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### date_format:_format_,...

The field under validation must match one of the given _formats_. You should use **either** `date` or `date_format` when validating a field, not both. This validation rule supports all formats supported by PHP's [DateTime](https://www.php.net/manual/en/class.datetime.php) class.

For convenience, date-based rules may be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->format('Y-m-d'),
],
```

---

## Pest3 Now Available New Configuration API
Pest3 Now Available | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest3-now-available.md

## New Configuration API

Pest 1 / Pest 2's configuration API was a little bit confusing, the `uses()` function that was originally made only for having the `$this` variable within closure bound to the test case instance, ended up being used for pretty much everything.

In Pest 3, we've introduced a new configuration API that is more intuitive and easier to use. The new configuration API is based on the `pest()` function, which allows you to configure Pest using a fluent and expressive API.

> Note: the `uses()` function is still available in Pest 3, and we don't have plans to remove it. However, we recommend using the new configuration API for new projects.

```diff
-uses(TestCase::class)->in(__DIR__);
+pest()->extends(TestCase::class);

-uses(TestCase::class, RefreshDatabase::class)->in('Features');
+pest()->extends(TestCase::class)->use(RefreshDatabase::class)->in('Features');

-uses()->compact();
+pest()->printer()->compact();
```

And of course, any method that was available on the `uses()` API, like `->beforeEach()` or `->group()` is still available on the new `pest()` configuration API; we've just made it more intuitive and easier to use.

---

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

#### assertInvalid

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

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

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

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

---

## Validation Available Validation Rules after:_date_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### after:_date_

The field under validation must be a value after a given date. The dates will be passed into the `strtotime` PHP function in order to be converted to a valid `DateTime` instance:

```php
'start_date' => 'required|date|after:tomorrow'
```

Instead of passing a date string to be evaluated by `strtotime`, you may specify another field to compare against the date:

```php
'finish_date' => 'required|date|after:start_date'
```

For convenience, date-based rules may be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->after(today()->addDays(7)),
],
```

The `afterToday` and `todayOrAfter` methods may be used to fluently express the date and must be after today, or today or after, respectively:

```php
'start_date' => [
    'required',
    Rule::date()->afterToday(),
],
```

---

## Validation Available Validation Rules date_equals:_date_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### date_equals:_date_

The field under validation must be equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance.

---

## Cli Api Reference
Cli Api Reference | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/cli-api-reference.md

---
title: CLI API Reference
description: In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.
---

# CLI API Reference

In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.

## Configuration

- `--init`: Initialize a standard Pest configuration.
- `--bootstrap <file>` A PHP script that is included before the tests run.
- `-c|--configuration <file>`: Read configuration from XML file.
- `--no-configuration`: Ignore default configuration file (phpunit.xml).
- `--extension <class>`: Register test runner extension with bootstrap <class>.
- `--no-extensions`: Do not load PHPUnit extensions.
- `--include-path <path(s)>`: Prepend PHP's include_path with given path(s).
- `-d <key[=value]>`: Set a php.ini value.
- `--cache-directory <dir>`: Specify cache directory.
- `--generate-configuration`: Generate configuration file with suggested settings.
- `--migrate-configuration`: Migrate configuration file to current format.
- `--generate-baseline <file>`: Generate baseline for issues.
- `--use-baseline <file>`: Use baseline to ignore issues.
- `--ignore-baseline`: Do not use baseline to ignore issues.
- `--test-directory`: Specify test directory containing Pest.php, TestCase.php, helpers and your tests. Default: tests

## Selection

- `--bail`: Stop execution upon first not-passed test.
- `--ci`: Ignore focused tests using `->only()` and run the entire test suite.
- `--todos`: Output to standard output the list of todos.
- `--notes`: Output to standard output tests with notes.
- `--issue`: Output to standard output tests with the given issue number.
- `--pr`: Output to standard output tests with the given pull request number.
- `--pull-request`: Output to standard output tests with the given pull request number (alias for `--pr`).
- `--flaky`: Output to standard output tests marked as flaky.
- `--retry`: Run non-passing tests first and stop execution upon first error or failure.
- `--list-suites` List available test suites.
- `--testsuite <name>`: Only run tests from the specified test suite(s).
- `--exclude-testsuite <name>`: Exclude tests from the specified test suite(s).
- `--list-groups`: List available test groups.
- `--group <name>`: Only run tests from the specified group(s).
- `--exclude-group <name>`: Exclude tests from the specified group(s).
- `--covers <name>`: Only run tests that intend to cover `<name>`.
- `--uses <name>`: Only run tests that intend to use `<name>`.
- `--requires-php-extension <name>`: Only run tests that require PHP extension <name>.
- `--list-test-files`: List available test files.
- `--list-tests`: List available tests.
- `--list-tests-xml <file>`: List available tests in XML format.
- `--filter <pattern>`: Filter which tests to run
- `--exclude-filter <pattern>`: Exclude tests for the specified filter pattern.
- `--test-suffix <suffixes>`: Only search for test in files with specified suffix(es). Default: Test.php,.phpt

## Execution

- `--parallel` Run tests in parallel.
- `--shard <index/total>`: Run only the given shard of tests (e.g. `--shard=1/5`). Uses time-balanced distribution when `tests/.pest/shards.json` exists.
- `--update-shards`: Update `tests/.pest/shards.json` with test timing data for time-balanced sharding. Can be combined with `--parallel`.
- `--update-snapshots`: Update snapshots for tests using the "toMatchSnapshot" expectation.
- `--globals-backup`: Backup and restore $GLOBALS for each test.
- `--static-backup`: Backup and restore static properties for each test.
- `--strict-coverage`: Be strict about code coverage metadata.
- `--strict-global-state`: Be strict about changes to global state.
- `--disallow-test-output`: Be strict about output during tests.
- `--enforce-time-limit`: Enforce time limit based on test size.
- `--default-time-limit <sec>`: Timeout in seconds for tests that have no declared size.
- `--dont-report-useless-tests`: Do not report tests that do not test anything.
- `--stop-on-defect`: Stop execution upon first not-passed test.
- `--stop-on-error`: Stop execution upon first error.
- `--stop-on-failure`: Stop execution upon first error or failure.
- `--stop-on-warning`: Stop execution upon first warning.
- `--stop-on-risky`: Stop execution upon first risky test.
- `--stop-on-deprecation`: Stop after first test that triggered a deprecation.
- `--stop-on-notice`: Stop after first test that triggered a notice.
- `--stop-on-skipped`: Stop execution upon first skipped test.
- `--stop-on-incomplete`: Stop execution upon first incomplete test.
- `--fail-on-empty-test-suite`: Signal failure using shell exit code when no tests were run.
- `--fail-on-warning`: Treat tests with warnings as failures.
- `--fail-on-risky`: Treat risky tests as failures.
- `--fail-on-deprecation`: Signal failure using shell exit code when a deprecation was triggered.
- `--fail-on-phpunit-deprecation`: Signal failure using shell exit code when a PHPUnit deprecation was triggered.
- `--fail-on-notice`: Signal failure using shell exit code when a notice was triggered.
- `--fail-on-skipped`: Treat skipped tests as failures.
- `--fail-on-incomplete`: Signal failure using shell exit code when a test was marked incomplete.
- `--cache-result`: Write test results to cache file.
- `--do-not-cache-result`: Do not write test results to cache file
- `--order-by <order>`: Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size.
- `--random-order-seed <N>`: Use the specified random seed when running tests in random order

## Reporting

- `--colors <flag>`: Use colors in output ("never", "auto" or "always").
- `--columns <n>`: Number of columns to use for progress output.
- `--columns max`: Use maximum number of columns for progress output.
- `--stderr`: Write to STDERR instead of STDOUT.
- `--no-progress`: Disable output of test execution progress.
- `--no-results`: Disable output of test results.
- `--no-output`: Disable all output.
- `--display-incomplete`: Display details for incomplete tests.
- `--display-skipped`: Display details for skipped tests.
- `--display-deprecations`: Display details for deprecations triggered by tests.
- `--display-phpunit-deprecations`: Display details for PHPUnit deprecations.
- `--display-errors`: Display details for errors triggered by tests.
- `--display-notices`: Display details for notices triggered by tests.
- `--display-warnings`: Display details for warnings triggered by tests.
- `--reverse-list`: Print defects in reverse order.
- `--teamcity`: Replace default progress and result output with TeamCity format.
- `--testdox`: Replace default result output with TestDox format
- `--testdox-summary`: Repeat TestDox output for tests with errors, failures, or issues.
- `--debug`: Replace default progress and result output with debugging information.
- `--compact`:  Replace default result output with Compact format

## Logging

- `--log-junit <file>`: Write test results in JUnit XML format to file.
- `--log-teamcity <file>`: Write test results in TeamCity format to file.
- `--testdox-html <file>`: Write test results in TestDox format (HTML) to file.
- `--testdox-text <file>`: Write test results in TestDox format (plain text) to file.
- `--log-events-text <file>`: Stream events as plain text to file.
- `--log-events-verbose-text <file>`: Stream events as plain text (with telemetry information) to file.
- `--no-logging`: Ignore logging configured in the XML configuration file

## Code Coverage

- `--coverage`: Generate code coverage report and output to standard output.
- `--coverage --min=<value>`: Set the minimum required coverage percentage, and fail if not met.
- `--coverage --exactly=<value>`: Set the exact required coverage percentage, and fail if not met.
- `--coverage --only-covered`: Hide files with 0% coverage from the code coverage report.
- `--coverage-clover <file>`: Write code coverage report in Clover XML format to file.
- `--coverage-cobertura <file>`: Write code coverage report in Cobertura XML format to file.
- `--coverage-crap4j <file>`: Write code coverage report in Crap4J XML format to file.
- `--coverage-html <dir>`: Write code coverage report in HTML format to directory.
- `--coverage-php <file>`: Write serialized code coverage data to file.
- `--coverage-text=<file>`: Write code coverage report in text format to file [default: standard output].
- `--only-summary-for-coverage-text`: Option for code coverage report in text format: only show summary.
- `--show-uncovered-for-coverage-text`: Option for code coverage report in text format: show uncovered files.
- `--coverage-xml <dir>`: Write code coverage report in XML format to directory.
- `--warm-coverage-cache`: Warm static analysis cache.
- `--coverage-filter <dir>`: Include `<dir>`: in code coverage reporting.
- `--path-coverage`: Report path coverage in addition to line coverage.
- `--disable-coverage-ignore`: Disable metadata for ignoring code coverage.
- `--no-coverage`: Ignore code coverage reporting configured in the XML configuration file

## Mutation Testing

- `--mutate`: Runs mutation testing, to understand the quality of your tests.
- `--mutate --parallel`: Runs mutation testing in parallel.
- `--mutate --min`: Set the minimum required mutation score, and fail if not met.
- `--mutate --id`:  Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run.
- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.
- `--mutate --bail`: Stop mutation testing execution upon first untested or uncovered mutation.
- `--mutate --class`: Generate mutations for the given class(es). E.g. --class=App\\Models.
- `--mutate --ignore`: Ignore the given class(es) when generating mutations. E.g. --ignore=App\\Http\\Requests.
- `--mutate --clear-cache`: Clear the mutation cache.
- `--mutate --no-cache`: Clear the mutation cache.
- `--mutate --ignore-min-score-on-zero-mutations`: Ignore the minimum score requirement when there are no mutations.
- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.
- `--mutate --everything`: Generate mutations for all classes, even if they are not covered by tests.
- `--mutate --profile`: Output to standard output the top ten slowest mutations.
- `--mutate --retry`: Run untested or uncovered mutations first and stop execution upon first error or failure.
- `--mutate --stop-on-uncovered`: Stop mutation testing execution upon first untested mutation.
- `--mutate --stop-on-untested`: Stop mutation testing execution upon first untested mutation.

## Profiling

- `--profile`: Output to standard output the top ten slowest tests

---

In this chapter, you found a complete list of CLI options provided by Pest. In the subsequent documentation, we will explore the topic of test dependencies: [Test Dependencies](/docs/test-dependencies)

---

## Validation Custom Validation Rules Implicit Rules
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Implicit Rules

By default, when an attribute being validated is not present or contains an empty string, normal validation rules, including custom rules, are not run. For example, the [unique](#rule-unique) rule will not be run against an empty string:

```php
use Illuminate\Support\Facades\Validator;

$rules = ['name' => 'unique:users,name'];

$input = ['name' => ''];

Validator::make($input, $rules)->passes(); // true
```

For a custom rule to run even when an attribute is empty, the rule must imply that the attribute is required. To quickly generate a new implicit rule object, you may use the `make:rule` Artisan command with the `--implicit` option:

```shell
php artisan make:rule Uppercase --implicit
```

> [!WARNING]
> An "implicit" rule only _implies_ that the attribute is required. Whether it actually invalidates a missing or empty attribute is up to you.

---

## Http Tests Testing JSON APIs
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

## Testing JSON APIs

Laravel 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:

```php tab=Pest
<?php

test('making an api request', function () {
    $response = $this->postJson('/api/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertJson([
            'created' => true,
        ]);
});
```

```php tab=PHPUnit
<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_making_an_api_request(): void
    {
        $response = $this->postJson('/api/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertJson([
                'created' => true,
            ]);
    }
}
```

In 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:

```php tab=Pest
expect($response['created'])->toBeTrue();
```

```php tab=PHPUnit
$this->assertTrue($response['created']);
```

> [!NOTE]
> 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.

---

## Validation Available Validation Rules prohibited_if_accepted:_anotherfield_,...
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### prohibited_if_accepted:_anotherfield_,...

The field under validation must be missing or empty if the _anotherfield_ field is equal to `"yes"`, `"on"`, `1`, `"1"`, `true`, or `"true"`.

---

## Validation Available Validation Rules date
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### date

The field under validation must be a valid, non-relative date according to the `strtotime` PHP function.

---

## Validation Available Validation Rules after\_or\_equal:_date_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### after\_or\_equal:_date_

The field under validation must be a value after or equal to the given date. For more information, see the [after](#rule-after) rule.

For convenience, date-based rules may be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->afterOrEqual(today()->addDays(7)),
],
```

---

## Validation Available Validation Rules before\_or\_equal:_date_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### before\_or\_equal:_date_

The field under validation must be a value preceding or equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`.

For convenience, date-based rules may also be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->beforeOrEqual(today()->subDays(7)),
],
```

---

## Sanctum Mobile Application Authentication Issuing API Tokens
Sanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md

### Issuing API Tokens

To get started, create a route that accepts the user's email / username, password, and device name, then exchanges those credentials for a new Sanctum token. The "device name" given to this endpoint is for informational purposes and may be any value you wish. In general, the device name value should be a name the user would recognize, such as "Nuno's iPhone 17".

Typically, you will make a request to the token endpoint from your mobile application's "login" screen. The endpoint will return the plain-text API token which may then be stored on the mobile device and used to make additional API requests:

```php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::post('/sanctum/token', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return $user->createToken($request->device_name)->plainTextToken;
});
```

When the mobile application uses the token to make an API request to your application, it should pass the token in the `Authorization` header as a `Bearer` token.

> [!NOTE]
> When issuing tokens for a mobile application, you are also free to specify [token abilities](#token-abilities).

---

## Validation Available Validation Rules before:_date_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### before:_date_

The field under validation must be a value preceding the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`.

For convenience, date-based rules may also be constructed using the fluent `date` rule builder:

```php
use Illuminate\Validation\Rule;

'start_date' => [
    'required',
    Rule::date()->before(today()->subDays(7)),
],
```

The `beforeToday` and `todayOrBefore` methods may be used to fluently express the date and must be before today, or today or before, respectively:

```php
'start_date' => [
    'required',
    Rule::date()->beforeToday(),
],
```

---

## Validation Custom Validation Rules Using Rule Objects
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Using Rule Objects

Laravel provides a variety of helpful validation rules; however, you may wish to specify some of your own. One method of registering custom validation rules is using rule objects. To generate a new rule object, you may use the `make:rule` Artisan command. Let's use this command to generate a rule that verifies a string is uppercase. Laravel will place the new rule in the `app/Rules` directory. If this directory does not exist, Laravel will create it when you execute the Artisan command to create your rule:

```shell
php artisan make:rule Uppercase
```

Once the rule has been created, we are ready to define its behavior. A rule object contains a single method: `validate`. This method receives the attribute name, its value, and a callback that should be invoked on failure with the validation error message:

```php
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Uppercase implements ValidationRule
{
    /**
     * Run the validation rule.
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
```

Once the rule has been defined, you may attach it to a validator by passing an instance of the rule object with your other validation rules:

```php
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
```

#### Translating Validation Messages

Instead of providing a literal error message to the `$fail` closure, you may also provide a [translation string key](/docs/{{version}}/localization) and instruct Laravel to translate the error message:

```php
if (strtoupper($value) !== $value) {
    $fail('validation.uppercase')->translate();
}
```

If necessary, you may provide placeholder replacements and the preferred language as the first and second arguments to the `translate` method:

```php
$fail('validation.location')->translate([
    'value' => $this->value,
], 'fr');
```

#### Accessing Additional Data

If your custom validation rule class needs to access all of the other data undergoing validation, your rule class may implement the `Illuminate\Contracts\Validation\DataAwareRule` interface. This interface requires your class to define a `setData` method. This method will automatically be invoked by Laravel (before validation proceeds) with all of the data under validation:

```php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;

class Uppercase implements DataAwareRule, ValidationRule
{
    /**
     * All of the data under validation.
     *
     * @var array<string, mixed>
     */
    protected $data = [];

    // ...

    /**
     * Set the data under validation.
     *
     * @param  array<string, mixed>  $data
     */
    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }
}
```

Or, if your validation rule requires access to the validator instance performing the validation, you may implement the `ValidatorAwareRule` interface:

```php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Validation\Validator;

class Uppercase implements ValidationRule, ValidatorAwareRule
{
    /**
     * The validator instance.
     *
     * @var \Illuminate\Validation\Validator
     */
    protected $validator;

    // ...

    /**
     * Set the current validator.
     */
    public function setValidator(Validator $validator): static
    {
        $this->validator = $validator;

        return $this;
    }
}
```

---

## Http Tests Testing JSON APIs Fluent JSON Testing Asserting Attribute Presence / Absence
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### Asserting Attribute Presence / Absence

To assert that an attribute is present or absent, you may use the `has` and `missing` methods:

```php
$response->assertJson(fn (AssertableJson $json) =>
    $json->has('data')
        ->missing('message')
);
```

In addition, the `hasAll` and `missingAll` methods allow asserting the presence or absence of multiple attributes simultaneously:

```php
$response->assertJson(fn (AssertableJson $json) =>
    $json->hasAll(['status', 'data'])
        ->missingAll(['message', 'code'])
);
```

You may use the `hasAny` method to determine if at least one of a given list of attributes is present:

```php
$response->assertJson(fn (AssertableJson $json) =>
    $json->has('status')
        ->hasAny('data', 'message', 'code')
);
```

---

## Sanctum API Token Authentication Token Expiration
Sanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md

### Token Expiration

By default, Sanctum tokens never expire and may only be invalidated by [revoking the token](#revoking-tokens). However, if you would like to configure an expiration time for your application's API tokens, you may do so via the `expiration` configuration option defined in your application's `sanctum` configuration file. This configuration option defines the number of minutes until an issued token will be considered expired:

```php
'expiration' => 525600,
```

If you would like to specify the expiration time of each token independently, you may do so by providing the expiration time as the third argument to the `createToken` method:

```php
return $user->createToken(
    'token-name', ['*'], now()->plus(weeks: 1)
)->plainTextToken;
```

If you have configured a token expiration time for your application, you may also wish to [schedule a task](/docs/{{version}}/scheduling) to prune your application's expired tokens. Thankfully, Sanctum includes a `sanctum:prune-expired` Artisan command that you may use to accomplish this. For example, you may configure a scheduled task to delete all expired token database records that have been expired for at least 24 hours:

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=24')->daily();
```

---

## Validation Custom Validation Rules Using Closures
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Using Closures

If you only need the functionality of a custom rule once throughout your application, you may use a closure instead of a rule object. The closure receives the attribute's name, the attribute's value, and a `$fail` callback that should be called if validation fails:

```php
use Illuminate\Support\Facades\Validator;
use Closure;

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value === 'foo') {
                $fail("The {$attribute} is invalid.");
            }
        },
    ],
]);
```

---

## Validation Available Validation Rules prohibited_if_declined:_anotherfield_,...
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### prohibited_if_declined:_anotherfield_,...

The field under validation must be missing or empty if the _anotherfield_ field is equal to `"no"`, `"off"`, `0`, `"0"`, `false`, or `"false"`.

---

## Sanctum API Token Authentication Protecting Routes
Sanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md

### Protecting Routes

To protect routes so that all incoming requests must be authenticated, you should attach the `sanctum` authentication guard to your protected routes within your `routes/web.php` and `routes/api.php` route files. This guard will ensure that incoming requests are authenticated as either stateful, cookie authenticated requests or contain a valid API token header if the request is from a third party.

You may be wondering why we suggest that you authenticate the routes within your application's `routes/web.php` file using the `sanctum` guard. Remember, Sanctum will first attempt to authenticate incoming requests using Laravel's typical session authentication cookie. If that cookie is not present then Sanctum will attempt to authenticate the request using a token in the request's `Authorization` header. In addition, authenticating all requests using Sanctum ensures that we may always call the `tokenCan` method on the currently authenticated user instance:

```php
use Illuminate\Http\Request;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');
```

_[Truncated]_


Output:
Wall time: 3.1291 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Validation Available Validation Rules date_format:_format_,...\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### date_format:_format_,...\n\nThe field under validation must match one of the given _formats_. You should use **either** `date` or `date_format` when validating a field, not both. This validation rule supports all formats supported by PHP's [DateTime](https://www.php.net/manual/en/class.datetime.php) class.\n\nFor convenience, date-based rules may be constructed using the fluent `date` rule builder:\n\n```php\nuse Illuminate\\Validation\\Rule;\n\n'start_date' => [\n    'required',\n    Rule::date()->format('Y-m-d'),\n],\n```\n\n---\n\n## Pest3 Now Available New Configuration API\nPest3 Now Available | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest3-now-available.md\n\n\n## New Configuration API\n\nPest 1 / Pest 2's configuration API was a little bit confusing, the `uses()` function that was originally made only for having the `$this` variable within closure bound to the test case instance, ended up being used for pretty much everything.\n\nIn Pest 3, we've introduced a new configuration API that is more intuitive and easier to use. The new configuration API is based on the `pest()` function, which allows you to configure Pest using a fluent and expressive API.\n\n> Note: the `uses()` function is still available in Pest 3, and we don't have plans to remove it. However, we recommend using the new configuration API for new projects.\n\n```diff\n-uses(TestCase::class)->in(__DIR__);\n+pest()->extends(TestCase::class);\n\n-uses(TestCase::class, RefreshDatabase::class)->in('Features');\n+pest()->extends(TestCase::class)->use(RefreshDatabase::class)->in('Features');\n\n-uses()->compact();\n+pest()->printer()->compact();\n```\n\nAnd of course, any method that was available on the `uses()` API, like `->beforeEach()` or `->group()` is still available on the new `pest()` configuration API; we've just made it more intuitive and easier to use.\n\n---\n\n## Http Tests Validation Assertions assertInvalid\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertInvalid\n\nAssert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:\n\n```php\n$response->assertInvalid(['name', 'email']);\n```\n\nYou may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:\n\n```php\n$response->assertInvalid([\n    'name' => 'The name field is required.',\n    'email' => 'valid email address',\n]);\n```\n\n---\n\n## Validation Available Validation Rules after:_date_\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### after:_date_\n\nThe field under validation must be a value after a given date. The dates will be passed into the `strtotime` PHP function in order to be converted to a valid `DateTime` instance:\n\n```php\n'start_date' => 'required|date|after:tomorrow'\n```\n\nInstead of passing a date string to be evaluated by `strtotime`, you may specify another field to compare against the date:\n\n```php\n'finish_date' => 'required|date|after:start_date'\n```\n\nFor convenience, date-based rules may be constructed using the fluent `date` rule builder:\n\n```php\nuse Illuminate\\Validation\\Rule;\n\n'start_date' => [\n    'required',\n    Rule::date()->after(today()->addDays(7)),\n],\n```\n\nThe `afterToday` and `todayOrAfter` methods may be used to fluently express the date and must be after today, or today or after, respectively:\n\n```php\n'start_date' => [\n    'required',\n    Rule::date()->afterToday(),\n],\n```\n\n---\n\n## Validation Available Validation Rules date_equals:_date_\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### date_equals:_date_\n\nThe field under validation must be equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance.\n\n---\n\n## Cli Api Reference\nCli Api Reference | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/cli-api-reference.md\n\n---\ntitle: CLI API Reference\ndescription: In the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.\n---\n\n# CLI API Reference\n\nIn the preceding chapters of the Pest documentation, we have covered numerous CLI options that are available in Pest. Nevertheless, Pest provides many other options that could prove beneficial. The complete CLI API Reference is provided below for your convenience.\n\n## Configuration\n\n- `--init`: Initialize a standard Pest configuration.\n- `--bootstrap <file>` A PHP script that is included before the tests run.\n- `-c|--configuration <file>`: Read configuration from XML file.\n- `--no-configuration`: Ignore default configuration file (phpunit.xml).\n- `--extension <class>`: Register test runner extension with bootstrap <class>.\n- `--no-extensions`: Do not load PHPUnit extensions.\n- `--include-path <path(s)>`: Prepend PHP's include_path with given path(s).\n- `-d <key[=value]>`: Set a php.ini value.\n- `--cache-directory <dir>`: Specify cache directory.\n- `--generate-configuration`: Generate configuration file with suggested settings.\n- `--migrate-configuration`: Migrate configuration file to current format.\n- `--generate-baseline <file>`: Generate baseline for issues.\n- `--use-baseline <file>`: Use baseline to ignore issues.\n- `--ignore-baseline`: Do not use baseline to ignore issues.\n- `--test-directory`: Specify test directory containing Pest.php, TestCase.php, helpers and your tests. Default: tests\n\n## Selection\n\n- `--bail`: Stop execution upon first not-passed test.\n- `--ci`: Ignore focused tests using `->only()` and run the entire test suite.\n- `--todos`: Output to standard output the list of todos.\n- `--notes`: Output to standard output tests with notes.\n- `--issue`: Output to standard output tests with the given issue number.\n- `--pr`: Output to standard output tests with the given pull request number.\n- `--pull-request`: Output to standard output tests with the given pull request number (alias for `--pr`).\n- `--flaky`: Output to standard output tests marked as flaky.\n- `--retry`: Run non-passing tests first and stop execution upon first error or failure.\n- `--list-suites` List available test suites.\n- `--testsuite <name>`: Only run tests from the specified test suite(s).\n- `--exclude-testsuite <name>`: Exclude tests from the specified test suite(s).\n- `--list-groups`: List available test groups.\n- `--group <name>`: Only run tests from the specified group(s).\n- `--exclude-group <name>`: Exclude tests from the specified group(s).\n- `--covers <name>`: Only run tests that intend to cover `<name>`.\n- `--uses <name>`: Only run tests that intend to use `<name>`.\n- `--requires-php-extension <name>`: Only run tests that require PHP extension <name>.\n- `--list-test-files`: List available test files.\n- `--list-tests`: List available tests.\n- `--list-tests-xml <file>`: List available tests in XML format.\n- `--filter <pattern>`: Filter which tests to run\n- `--exclude-filter <pattern>`: Exclude tests for the specified filter pattern.\n- `--test-suffix <suffixes>`: Only search for test in files with specified suffix(es). Default: Test.php,.phpt\n\n## Execution\n\n- `--parallel` Run tests in parallel.\n- `--shard <index/total>`: Run only the given shard of tests (e.g. `--shard=1/5`). Uses time-balanced distribution when `tests/.pest/shards.json` exists.\n- `--update-shards`: Update `tests/.pest/shards.json` with test timing data for time-balanced sharding. Can be combined with `--parallel`.\n- `--update-snapshots`: Update snapshots for tests using the \"toMatchSnapshot\" expectation.\n- `--globals-backup`: Backup and restore $GLOBALS for each test.\n- `--static-backup`: Backup and restore static properties for each test.\n- `--strict-coverage`: Be strict about code coverage metadata.\n- `--strict-global-state`: Be strict about changes to global state.\n- `--disallow-test-output`: Be strict about output during tests.\n- `--enforce-time-limit`: Enforce time limit based on test size.\n- `--default-time-limit <sec>`: Timeout in seconds for tests that have no declared size.\n- `--dont-report-useless-tests`: Do not report tests that do not test anything.\n- `--stop-on-defect`: Stop execution upon first not-passed test.\n- `--stop-on-error`: Stop execution upon first error.\n- `--stop-on-failure`: Stop execution upon first error or failure.\n- `--stop-on-warning`: Stop execution upon first warning.\n- `--stop-on-risky`: Stop execution upon first risky test.\n- `--stop-on-deprecation`: Stop after first test that triggered a deprecation.\n- `--stop-on-notice`: Stop after first test that triggered a notice.\n- `--stop-on-skipped`: Stop execution upon first skipped test.\n- `--stop-on-incomplete`: Stop execution upon first incomplete test.\n- `--fail-on-empty-test-suite`: Signal failure using shell exit code when no tests were run.\n- `--fail-on-warning`: Treat tests with warnings as failures.\n- `--fail-on-risky`: Treat risky tests as failures.\n- `--fail-on-deprecation`: Signal failure using shell exit code when a deprecation was triggered.\n- `--fail-on-phpunit-deprecation`: Signal failure using shell exit code when a PHPUnit deprecation was triggered.\n- `--fail-on-notice`: Signal failure using shell exit code when a notice was triggered.\n- `--fail-on-skipped`: Treat skipped tests as failures.\n- `--fail-on-incomplete`: Signal failure using shell exit code when a test was marked incomplete.\n- `--cache-result`: Write test results to cache file.\n- `--do-not-cache-result`: Do not write test results to cache file\n- `--order-by <order>`: Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size.\n- `--random-order-seed <N>`: Use the specified random seed when running tests in random order\n\n## Reporting\n\n- `--colors <flag>`: Use colors in output (\"never\", \"auto\" or \"always\").\n- `--columns <n>`: Number of columns to use for progress output.\n- `--columns max`: Use maximum number of columns for progress output.\n- `--stderr`: Write to STDERR instead of STDOUT.\n- `--no-progress`: Disable output of test execution progress.\n- `--no-results`: Disable output of test results.\n- `--no-output`: Disable all output.\n- `--display-incomplete`: Display details for incomplete tests.\n- `--display-skipped`: Display details for skipped tests.\n- `--display-deprecations`: Display details for deprecations triggered by tests.\n- `--display-phpunit-deprecations`: Display details for PHPUnit deprecations.\n- `--display-errors`: Display details for errors triggered by tests.\n- `--display-notices`: Display details for notices triggered by tests.\n- `--display-warnings`: Display details for warnings triggered by tests.\n- `--reverse-list`: Print defects in reverse order.\n- `--teamcity`: Replace default progress and result output with TeamCity format.\n- `--testdox`: Replace default result output with TestDox format\n- `--testdox-summary`: Repeat TestDox output for tests with errors, failures, or issues.\n- `--debug`: Replace default progress and result output with debugging information.\n- `--compact`:  Replace default result output with Compact format\n\n## Logging\n\n- `--log-junit <file>`: Write test results in JUnit XML format to file.\n- `--log-teamcity <file>`: Write test results in TeamCity format to file.\n- `--testdox-html <file>`: Write test results in TestDox format (HTML) to file.\n- `--testdox-text <file>`: Write test results in TestDox format (plain text) to file.\n- `--log-events-text <file>`: Stream events as plain text to file.\n- `--log-events-verbose-text <file>`: Stream events as plain text (with telemetry information) to file.\n- `--no-logging`: Ignore logging configured in the XML configuration file\n\n## Code Coverage\n\n- `--coverage`: Generate code coverage report and output to standard output.\n- `--coverage --min=<value>`: Set the minimum required coverage percentage, and fail if not met.\n- `--coverage --exactly=<value>`: Set the exact required coverage percentage, and fail if not met.\n- `--coverage --only-covered`: Hide files with 0% coverage from the code coverage report.\n- `--coverage-clover <file>`: Write code coverage report in Clover XML format to file.\n- `--coverage-cobertura <file>`: Write code coverage report in Cobertura XML format to file.\n- `--coverage-crap4j <file>`: Write code coverage report in Crap4J XML format to file.\n- `--coverage-html <dir>`: Write code coverage report in HTML format to directory.\n- `--coverage-php <file>`: Write serialized code coverage data to file.\n- `--coverage-text=<file>`: Write code coverage report in text format to file [default: standard output].\n- `--only-summary-for-coverage-text`: Option for code coverage report in text format: only show summary.\n- `--show-uncovered-for-coverage-text`: Option for code coverage report in text format: show uncovered files.\n- `--coverage-xml <dir>`: Write code coverage report in XML format to directory.\n- `--warm-coverage-cache`: Warm static analysis cache.\n- `--coverage-filter <dir>`: Include `<dir>`: in code coverage reporting.\n- `--path-coverage`: Report path coverage in addition to line coverage.\n- `--disable-coverage-ignore`: Disable metadata for ignoring code coverage.\n- `--no-coverage`: Ignore code coverage reporting configured in the XML configuration file\n\n## Mutation Testing\n\n- `--mutate`: Runs mutation testing, to understand the quality of your tests.\n- `--mutate --parallel`: Runs mutation testing in parallel.\n- `--mutate --min`: Set the minimum required mutation score, and fail if not met.\n- `--mutate --id`:  Run only the mutation with the given ID. But E.g. --id=ecb35ab30ffd3491. Note, you need to provide the same options as the original run.\n- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.\n- `--mutate --bail`: Stop mutation testing execution upon first untested or uncovered mutation.\n- `--mutate --class`: Generate mutations for the given class(es). E.g. --class=App\\\\Models.\n- `--mutate --ignore`: Ignore the given class(es) when generating mutations. E.g. --ignore=App\\\\Http\\\\Requests.\n- `--mutate --clear-cache`: Clear the mutation cache.\n- `--mutate --no-cache`: Clear the mutation cache.\n- `--mutate --ignore-min-score-on-zero-mutations`: Ignore the minimum score requirement when there are no mutations.\n- `--mutate --covered-only`: Only generate mutations for classes that are covered by tests.\n- `--mutate --everything`: Generate mutations for all classes, even if they are not covered by tests.\n- `--mutate --profile`: Output to standard output the top ten slowest mutations.\n- `--mutate --retry`: Run untested or uncovered mutations first and stop execution upon first error or failure.\n- `--mutate --stop-on-uncovered`: Stop mutation testing execution upon first untested mutation.\n- `--mutate --stop-on-untested`: Stop mutation testing execution upon first untested mutation.\n\n## Profiling\n\n- `--profile`: Output to standard output the top ten slowest tests\n\n---\n\nIn this chapter, you found a complete list of CLI options provided by Pest. In the subsequent documentation, we will explore the topic of test dependencies: [Test Dependencies](/docs/test-dependencies)\n\n\n---\n\n## Validation Custom Validation Rules Implicit Rules\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Implicit Rules\n\nBy default, when an attribute being validated is not present or contains an empty string, normal validation rules, including custom rules, are not run. For example, the [unique](#rule-unique) rule will not be run against an empty string:\n\n```php\nuse Illuminate\\Support\\Facades\\Validator;\n\n$rules = ['name' => 'unique:users,name'];\n\n$input = ['name' => ''];\n\nValidator::make($input, $rules)->passes(); // true\n```\n\nFor a custom rule to run even when an attribute is empty, the rule must imply that the attribute is required. To quickly generate a new implicit rule object, you may use the `make:rule` Artisan command with the `--implicit` option:\n\n```shell\nphp artisan make:rule Uppercase --implicit\n```\n\n> [!WARNING]\n> An \"implicit\" rule only _implies_ that the attribute is required. Whether it actually invalidates a missing or empty attribute is up to you.\n\n---\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## Validation Available Validation Rules prohibited_if_accepted:_anotherfield_,...\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### prohibited_if_accepted:_anotherfield_,...\n\nThe field under validation must be missing or empty if the _anotherfield_ field is equal to `\"yes\"`, `\"on\"`, `1`, `\"1\"`, `true`, or `\"true\"`.\n\n---\n\n## Validation Available Validation Rules date\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### date\n\nThe field under validation must be a valid, non-relative date according to the `strtotime` PHP function.\n\n---\n\n## Validation Available Validation Rules after\\_or\\_equal:_date_\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### after\\_or\\_equal:_date_\n\nThe field under validation must be a value after or equal to the given date. For more information, see the [after](#rule-after) rule.\n\nFor convenience, date-based rules may be constructed using the fluent `date` rule builder:\n\n```php\nuse Illuminate\\Validation\\Rule;\n\n'start_date' => [\n    'required',\n    Rule::date()->afterOrEqual(today()->addDays(7)),\n],\n```\n\n---\n\n## Validation Available Validation Rules before\\_or\\_equal:_date_\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### before\\_or\\_equal:_date_\n\nThe field under validation must be a value preceding or equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`.\n\nFor convenience, date-based rules may also be constructed using the fluent `date` rule builder:\n\n```php\nuse Illuminate\\Validation\\Rule;\n\n'start_date' => [\n    'required',\n    Rule::date()->beforeOrEqual(today()->subDays(7)),\n],\n```\n\n---\n\n## Sanctum Mobile Application Authentication Issuing API Tokens\nSanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md\n\n\n### Issuing API Tokens\n\nTo get started, create a route that accepts the user's email / username, password, and device name, then exchanges those credentials for a new Sanctum token. The \"device name\" given to this endpoint is for informational purposes and may be any value you wish. In general, the device name value should be a name the user would recognize, such as \"Nuno's iPhone 17\".\n\nTypically, you will make a request to the token endpoint from your mobile application's \"login\" screen. The endpoint will return the plain-text API token which may then be stored on the mobile device and used to make additional API requests:\n\n```php\nuse App\\Models\\User;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Validation\\ValidationException;\n\nRoute::post('/sanctum/token', function (Request $request) {\n    $request->validate([\n        'email' => 'required|email',\n        'password' => 'required',\n        'device_name' => 'required',\n    ]);\n\n    $user = User::where('email', $request->email)->first();\n\n    if (! $user || ! Hash::check($request->password, $user->password)) {\n        throw ValidationException::withMessages([\n            'email' => ['The provided credentials are incorrect.'],\n        ]);\n    }\n\n    return $user->createToken($request->device_name)->plainTextToken;\n});\n```\n\nWhen the mobile application uses the token to make an API request to your application, it should pass the token in the `Authorization` header as a `Bearer` token.\n\n> [!NOTE]\n> When issuing tokens for a mobile application, you are also free to specify [token abilities](#token-abilities).\n\n---\n\n## Validation Available Validation Rules before:_date_\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### before:_date_\n\nThe field under validation must be a value preceding the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`.\n\nFor convenience, date-based rules may also be constructed using the fluent `date` rule builder:\n\n```php\nuse Illuminate\\Validation\\Rule;\n\n'start_date' => [\n    'required',\n    Rule::date()->before(today()->subDays(7)),\n],\n```\n\nThe `beforeToday` and `todayOrBefore` methods may be used to fluently express the date and must be before today, or today or before, respectively:\n\n```php\n'start_date' => [\n    'required',\n    Rule::date()->beforeToday(),\n],\n```\n\n---\n\n## Validation Custom Validation Rules Using Rule Objects\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Using Rule Objects\n\nLaravel provides a variety of helpful validation rules; however, you may wish to specify some of your own. One method of registering custom validation rules is using rule objects. To generate a new rule object, you may use the `make:rule` Artisan command. Let's use this command to generate a rule that verifies a string is uppercase. Laravel will place the new rule in the `app/Rules` directory. If this directory does not exist, Laravel will create it when you execute the Artisan command to create your rule:\n\n```shell\nphp artisan make:rule Uppercase\n```\n\nOnce the rule has been created, we are ready to define its behavior. A rule object contains a single method: `validate`. This method receives the attribute name, its value, and a callback that should be invoked on failure with the validation error message:\n\n```php\n<?php\n\nnamespace App\\Rules;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass Uppercase implements ValidationRule\n{\n    /**\n     * Run the validation rule.\n     */\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (strtoupper($value) !== $value) {\n            $fail('The :attribute must be uppercase.');\n        }\n    }\n}\n```\n\nOnce the rule has been defined, you may attach it to a validator by passing an instance of the rule object with your other validation rules:\n\n```php\nuse App\\Rules\\Uppercase;\n\n$request->validate([\n    'name' => ['required', 'string', new Uppercase],\n]);\n```\n\n#### Translating Validation Messages\n\nInstead of providing a literal error message to the `$fail` closure, you may also provide a [translation string key](/docs/{{version}}/localization) and instruct Laravel to translate the error message:\n\n```php\nif (strtoupper($value) !== $value) {\n    $fail('validation.uppercase')->translate();\n}\n```\n\nIf necessary, you may provide placeholder replacements and the preferred language as the first and second arguments to the `translate` method:\n\n```php\n$fail('validation.location')->translate([\n    'value' => $this->value,\n], 'fr');\n```\n\n#### Accessing Additional Data\n\nIf your custom validation rule class needs to access all of the other data undergoing validation, your rule class may implement the `Illuminate\\Contracts\\Validation\\DataAwareRule` interface. This interface requires your class to define a `setData` method. This method will automatically be invoked by Laravel (before validation proceeds) with all of the data under validation:\n\n```php\n<?php\n\nnamespace App\\Rules;\n\nuse Illuminate\\Contracts\\Validation\\DataAwareRule;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass Uppercase implements DataAwareRule, ValidationRule\n{\n    /**\n     * All of the data under validation.\n     *\n     * @var array<string, mixed>\n     */\n    protected $data = [];\n\n    // ...\n\n    /**\n     * Set the data under validation.\n     *\n     * @param  array<string, mixed>  $data\n     */\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n\n        return $this;\n    }\n}\n```\n\nOr, if your validation rule requires access to the validator instance performing the validation, you may implement the `ValidatorAwareRule` interface:\n\n```php\n<?php\n\nnamespace App\\Rules;\n\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\nuse Illuminate\\Contracts\\Validation\\ValidatorAwareRule;\nuse Illuminate\\Validation\\Validator;\n\nclass Uppercase implements ValidationRule, ValidatorAwareRule\n{\n    /**\n     * The validator instance.\n     *\n     * @var \\Illuminate\\Validation\\Validator\n     */\n    protected $validator;\n\n    // ...\n\n    /**\n     * Set the current validator.\n     */\n    public function setValidator(Validator $validator): static\n    {\n        $this->validator = $validator;\n\n        return $this;\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## Sanctum API Token Authentication Token Expiration\nSanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md\n\n\n### Token Expiration\n\nBy default, Sanctum tokens never expire and may only be invalidated by [revoking the token](#revoking-tokens). However, if you would like to configure an expiration time for your application's API tokens, you may do so via the `expiration` configuration option defined in your application's `sanctum` configuration file. This configuration option defines the number of minutes until an issued token will be considered expired:\n\n```php\n'expiration' => 525600,\n```\n\nIf you would like to specify the expiration time of each token independently, you may do so by providing the expiration time as the third argument to the `createToken` method:\n\n```php\nreturn $user->createToken(\n    'token-name', ['*'], now()->plus(weeks: 1)\n)->plainTextToken;\n```\n\nIf you have configured a token expiration time for your application, you may also wish to [schedule a task](/docs/{{version}}/scheduling) to prune your application's expired tokens. Thankfully, Sanctum includes a `sanctum:prune-expired` Artisan command that you may use to accomplish this. For example, you may configure a scheduled task to delete all expired token database records that have been expired for at least 24 hours:\n\n```php\nuse Illuminate\\Support\\Facades\\Schedule;\n\nSchedule::command('sanctum:prune-expired --hours=24')->daily();\n```\n\n---\n\n## Validation Custom Validation Rules Using Closures\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Using Closures\n\nIf you only need the functionality of a custom rule once throughout your application, you may use a closure instead of a rule object. The closure receives the attribute's name, the attribute's value, and a `$fail` callback that should be called if validation fails:\n\n```php\nuse Illuminate\\Support\\Facades\\Validator;\nuse Closure;\n\n$validator = Validator::make($request->all(), [\n    'title' => [\n        'required',\n        'max:255',\n        function (string $attribute, mixed $value, Closure $fail) {\n            if ($value === 'foo') {\n                $fail(\"The {$attribute} is invalid.\");\n            }\n        },\n    ],\n]);\n```\n\n---\n\n## Validation Available Validation Rules prohibited_if_declined:_anotherfield_,...\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### prohibited_if_declined:_anotherfield_,...\n\nThe field under validation must be missing or empty if the _anotherfield_ field is equal to `\"no\"`, `\"off\"`, `0`, `\"0\"`, `false`, or `\"false\"`.\n\n---\n\n## Sanctum API Token Authentication Protecting Routes\nSanctum | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/sanctum.md\n\n\n### Protecting Routes\n\nTo protect routes so that all incoming requests must be authenticated, you should attach the `sanctum` authentication guard to your protected routes within your `routes/web.php` and `routes/api.php` route files. This guard will ensure that incoming requests are authenticated as either stateful, cookie authenticated requests or contain a valid API token header if the request is from a third party.\n\nYou may be wondering why we suggest that you authenticate the routes within your application's `routes/web.php` file using the `sanctum` guard. Remember, Sanctum will first attempt to authenticate incoming requests using Laravel's typical session authentication cookie. If that cookie is not present then Sanctum will attempt to authenticate the request using a token in the request's `Authorization` header. In addition, authenticating all requests using Sanctum ensures that we may always call the `tokenCan` method on the currently authenticated user instance:\n\n```php\nuse Illuminate\\Http\\Request;\n\nRoute::get('/user', function (Request $request) {\n    return $request->user();\n})->middleware('auth:sanctum');\n```\n\n_[Truncated]_"}]


php artisan make:controller Api/Admin/V1/DiscountController --no-interaction


php artisan make:resource Admin/V1/DiscountResource --no-interaction


php artisan make:test Api/AdminDiscountApiTest --pest --no-interaction


Output:
Chunk ID: 0857b5
Wall time: 0.0426 seconds
Process exited with code 0
Original token count: 27
Output:

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


Output:
Chunk ID: 1b4199
Wall time: 0.0350 seconds
Process exited with code 0
Original token count: 24
Output:

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


Output:
Chunk ID: edf69e
Wall time: 0.0326 seconds
Process exited with code 0
Original token count: 22
Output:

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


php artisan make:resource Admin/V1/DiscountResource --no-interaction

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


php artisan make:controller Api/Admin/V1/DiscountController --no-interaction

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


php artisan make:test Api/AdminDiscountApiTest --pest --no-interaction

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


Input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/DiscountController.php
*** End Patch


Success. Updated the following files:
D /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/DiscountController.php

<?php

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

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class DiscountController extends Controller
{
    //
}


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


Input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/DiscountController.php
+<?php
+
+namespace App\Http\Controllers\Api\Admin\V1;
+
+use App\Http\Controllers\Controller;
+use App\Http\Resources\Admin\V1\DiscountResource;
+use App\Models\Discount;
+use App\Models\Store;
+use Carbon\CarbonInterface;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use Illuminate\Support\Str;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\ValidationException;
+
+class DiscountController extends Controller
+{
+    public function index(Request $request, Store $store): AnonymousResourceCollection
+    {
+        $this->authorizeStore($request, $store);
+
+        $validated = $request->validate([
+            'type' => ['nullable', Rule::in(['code', 'automatic'])],
+            'status' => ['nullable', Rule::in(['active', 'expired', 'scheduled'])],
+            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
+        ]);
+
+        $discounts = Discount::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->when(data_get($validated, 'type'), fn (Builder $query, string $type) => $query->where('type', $type))
+            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $this->applyComputedStatus($query, $status))
+            ->latest('created_at')
+            ->latest('id')
+            ->paginate((int) data_get($validated, 'per_page', 25));
+
+        return DiscountResource::collection($discounts);
+    }
+
+    public function store(Request $request, Store $store): JsonResponse
+    {
+        $this->authorizeStore($request, $store);
+
+        $validated = $this->validatePayload($request, $store);
+
+        $discount = Discount::withoutGlobalScopes()->create($this->attributesForCreate($validated, $store));
+
+        return DiscountResource::make($discount)
+            ->response()
+            ->setStatusCode(201);
+    }
+
+    public function update(Request $request, Store $store, Discount $discount): DiscountResource
+    {
+        $this->authorizeStore($request, $store);
+        $this->abortUnlessDiscountBelongsToStore($discount, $store);
+
+        $validated = $this->validatePayload($request, $store, $discount);
+
+        $discount->update($this->attributesForUpdate($validated, $discount));
+
+        return DiscountResource::make($discount->refresh());
+    }
+
+    public function destroy(Request $request, Store $store, Discount $discount): JsonResponse
+    {
+        $this->authorizeStore($request, $store);
+        $this->abortUnlessDiscountBelongsToStore($discount, $store);
+
+        $discount->delete();
+
+        return response()->json(['message' => 'Discount deleted']);
+    }
+
+    private function authorizeStore(Request $request, Store $store): void
+    {
+        if (! $request->attributes->has('admin_api_oauth_token')) {
+            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
+        }
+
+        app()->instance('current_store', $store);
+    }
+
+    private function abortUnlessDiscountBelongsToStore(Discount $discount, Store $store): void
+    {
+        abort_unless((int) $discount->store_id === $store->getKey(), 404);
+    }
+
+    private function applyComputedStatus(Builder $query, string $status): void
+    {
+        match ($status) {
+            'active' => $query
+                ->where('status', 'active')
+                ->where('starts_at', '<=', now())
+                ->where(fn (Builder $query) => $query->whereNull('ends_at')->orWhere('ends_at', '>', now())),
+            'expired' => $query->where(fn (Builder $query) => $query->where('status', 'expired')->orWhere('ends_at', '<=', now())),
+            'scheduled' => $query->where('starts_at', '>', now()),
+        };
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    private function validatePayload(Request $request, Store $store, ?Discount $discount = null): array
+    {
+        $creating = $discount === null;
+        $type = (string) $request->input('type', $discount?->type->value ?? 'code');
+        $valueType = (string) $request->input('value_type', $discount?->value_type->value ?? 'percent');
+
+        $validated = $request->validate([
+            'type' => [$creating ? 'required' : 'sometimes', Rule::in(['code', 'automatic'])],
+            'code' => [
+                Rule::requiredIf($creating && $type === 'code'),
+                'nullable',
+                'string',
+                'max:50',
+                function (string $attribute, mixed $value, \Closure $fail) use ($store, $discount): void {
+                    if ($value === null || trim((string) $value) === '') {
+                        return;
+                    }
+
+                    $normalized = Str::upper(trim((string) $value));
+
+                    if ($discount instanceof Discount && $normalized !== (string) $discount->code) {
+                        $fail(__('The discount code cannot be changed after creation.'));
+
+                        return;
+                    }
+
+                    $exists = Discount::withoutGlobalScopes()
+                        ->where('store_id', $store->getKey())
+                        ->whereRaw('lower(code) = ?', [Str::lower($normalized)])
+                        ->when($discount instanceof Discount, fn (Builder $query) => $query->whereKeyNot($discount->getKey()))
+                        ->exists();
+
+                    if ($exists) {
+                        $fail(__('The discount code has already been taken.'));
+                    }
+                },
+            ],
+            'value_type' => [$creating ? 'required' : 'sometimes', Rule::in(['fixed', 'percent', 'free_shipping'])],
+            'value_amount' => $this->valueAmountRules($creating, $valueType),
+            'starts_at' => ['sometimes', 'nullable', 'date'],
+            'ends_at' => ['sometimes', 'nullable', 'date'],
+            'usage_limit' => ['sometimes', 'nullable', 'integer', 'min:1'],
+            'status' => ['sometimes', Rule::in(['draft', 'active', 'expired', 'disabled'])],
+            'rules_json' => ['sometimes', 'array'],
+            'rules_json.minimum_purchase_amount' => ['nullable', 'integer', 'min:0'],
+            'rules_json.min_purchase_amount' => ['nullable', 'integer', 'min:0'],
+            'rules_json.applicable_product_ids' => ['nullable', 'array'],
+            'rules_json.applicable_product_ids.*' => ['integer', Rule::exists('products', 'id')->where('store_id', $store->getKey())],
+            'rules_json.applicable_collection_ids' => ['nullable', 'array'],
+            'rules_json.applicable_collection_ids.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
+            'rules_json.customer_eligibility' => ['nullable', Rule::in(['all', 'specific_customers', 'specific_segments'])],
+            'rules_json.once_per_customer' => ['nullable', 'boolean'],
+            'rules_json.one_per_customer' => ['nullable', 'boolean'],
+        ]);
+
+        $this->validateDates($validated, $discount);
+
+        return $validated;
+    }
+
+    /**
+     * @return list<mixed>
+     */
+    private function valueAmountRules(bool $creating, string $valueType): array
+    {
+        $presence = $creating ? 'required' : 'sometimes';
+
+        return match ($valueType) {
+            'percent' => [$presence, 'integer', 'min:1', 'max:100'],
+            'free_shipping' => ['sometimes', 'nullable', 'integer', 'min:0'],
+            default => [$presence, 'integer', 'min:1'],
+        };
+    }
+
+    /**
+     * @param  array<string, mixed>  $validated
+     */
+    private function validateDates(array $validated, ?Discount $discount): void
+    {
+        if (! array_key_exists('ends_at', $validated) || $validated['ends_at'] === null) {
+            return;
+        }
+
+        $startsAt = data_get($validated, 'starts_at', $discount?->starts_at ?? now());
+
+        if (! $startsAt instanceof CarbonInterface) {
+            $startsAt = now()->parse($startsAt);
+        }
+
+        $endsAt = now()->parse($validated['ends_at']);
+
+        if ($endsAt->lte($startsAt)) {
+            throw ValidationException::withMessages([
+                'ends_at' => __('The end date must be after the start date.'),
+            ]);
+        }
+    }
+
+    /**
+     * @param  array<string, mixed>  $validated
+     * @return array<string, mixed>
+     */
+    private function attributesForCreate(array $validated, Store $store): array
+    {
+        $type = (string) $validated['type'];
+        $valueType = (string) $validated['value_type'];
+
+        return [
+            'store_id' => $store->getKey(),
+            'type' => $type,
+            'code' => $type === 'code' ? Str::upper(trim((string) $validated['code'])) : null,
+            'value_type' => $valueType,
+            'value_amount' => $valueType === 'free_shipping' ? 0 : (int) $validated['value_amount'],
+            'starts_at' => $validated['starts_at'] ?? now(),
+            'ends_at' => $validated['ends_at'] ?? null,
+            'usage_limit' => $validated['usage_limit'] ?? null,
+            'usage_count' => 0,
+            'rules_json' => $this->rulesPayload($validated['rules_json'] ?? []),
+            'status' => $validated['status'] ?? 'active',
+        ];
+    }
+
+    /**
+     * @param  array<string, mixed>  $validated
+     * @return array<string, mixed>
+     */
+    private function attributesForUpdate(array $validated, Discount $discount): array
+    {
+        $attributes = [];
+        $valueType = (string) data_get($validated, 'value_type', $discount->value_type->value);
+
+        foreach (['type', 'value_type', 'starts_at', 'ends_at', 'usage_limit', 'status'] as $field) {
+            if (array_key_exists($field, $validated)) {
+                $attributes[$field] = $validated[$field];
+            }
+        }
+
+        if (array_key_exists('value_amount', $validated)) {
+            $attributes['value_amount'] = $valueType === 'free_shipping' ? 0 : (int) $validated['value_amount'];
+        } elseif (array_key_exists('value_type', $validated) && $valueType === 'free_shipping') {
+            $attributes['value_amount'] = 0;
+        }
+
+        if (array_key_exists('rules_json', $validated)) {
+            $attributes['rules_json'] = $this->rulesPayload($validated['rules_json']);
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * @param  array<string, mixed>  $rules
+     * @return array<string, mixed>
+     */
+    private function rulesPayload(array $rules): array
+    {
+        return [
+            'min_purchase_amount' => (int) data_get($rules, 'min_purchase_amount', data_get($rules, 'minimum_purchase_amount', 0)),
+            'applicable_product_ids' => $this->integerList(data_get($rules, 'applicable_product_ids', [])),
+            'applicable_collection_ids' => $this->integerList(data_get($rules, 'applicable_collection_ids', [])),
+            'customer_eligibility' => data_get($rules, 'customer_eligibility', 'all'),
+            'one_per_customer' => (bool) data_get($rules, 'one_per_customer', data_get($rules, 'once_per_customer', false)),
+        ];
+    }
+
+    /**
+     * @return list<int>
+     */
+    private function integerList(mixed $value): array
+    {
+        return collect(is_array($value) ? $value : [])
+            ->map(fn (mixed $id): int => (int) $id)
+            ->unique()
+            ->values()
+            ->all();
+    }
+}
*** End Patch


Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/DiscountController.php

<?php

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

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\DiscountResource;
use App\Models\Discount;
use App\Models\Store;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;

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

        $validated = $request->validate([
            'type' => ['nullable', Rule::in(['code', 'automatic'])],
            'status' => ['nullable', Rule::in(['active', 'expired', 'scheduled'])],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $discounts = Discount::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'type'), fn (Builder $query, string $type) => $query->where('type', $type))
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $this->applyComputedStatus($query, $status))
            ->latest('created_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return DiscountResource::collection($discounts);
    }

    public function store(Request $request, Store $store): JsonResponse
    {
        $this->authorizeStore($request, $store);

        $validated = $this->validatePayload($request, $store);

        $discount = Discount::withoutGlobalScopes()->create($this->attributesForCreate($validated, $store));

        return DiscountResource::make($discount)
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Store $store, Discount $discount): DiscountResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessDiscountBelongsToStore($discount, $store);

        $validated = $this->validatePayload($request, $store, $discount);

        $discount->update($this->attributesForUpdate($validated, $discount));

        return DiscountResource::make($discount->refresh());
    }

    public function destroy(Request $request, Store $store, Discount $discount): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessDiscountBelongsToStore($discount, $store);

        $discount->delete();

        return response()->json(['message' => 'Discount deleted']);
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

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

    private function abortUnlessDiscountBelongsToStore(Discount $discount, Store $store): void
    {
        abort_unless((int) $discount->store_id === $store->getKey(), 404);
    }

    private function applyComputedStatus(Builder $query, string $status): void
    {
        match ($status) {
            'active' => $query
                ->where('status', 'active')
                ->where('starts_at', '<=', now())
                ->where(fn (Builder $query) => $query->whereNull('ends_at')->orWhere('ends_at', '>', now())),
            'expired' => $query->where(fn (Builder $query) => $query->where('status', 'expired')->orWhere('ends_at', '<=', now())),
            'scheduled' => $query->where('starts_at', '>', now()),
        };
    }

    /**
     * @return array<string, mixed>
     */
    private function validatePayload(Request $request, Store $store, ?Discount $discount = null): array
    {
        $creating = $discount === null;
        $type = (string) $request->input('type', $discount?->type->value ?? 'code');
        $valueType = (string) $request->input('value_type', $discount?->value_type->value ?? 'percent');

        $validated = $request->validate([
            'type' => [$creating ? 'required' : 'sometimes', Rule::in(['code', 'automatic'])],
            'code' => [
                Rule::requiredIf($creating && $type === 'code'),
                'nullable',
                'string',
                'max:50',
                function (string $attribute, mixed $value, \Closure $fail) use ($store, $discount): void {
                    if ($value === null || trim((string) $value) === '') {
                        return;
                    }

                    $normalized = Str::upper(trim((string) $value));

                    if ($discount instanceof Discount && $normalized !== (string) $discount->code) {
                        $fail(__('The discount code cannot be changed after creation.'));

                        return;
                    }

                    $exists = Discount::withoutGlobalScopes()
                        ->where('store_id', $store->getKey())
                        ->whereRaw('lower(code) = ?', [Str::lower($normalized)])
                        ->when($discount instanceof Discount, fn (Builder $query) => $query->whereKeyNot($discount->getKey()))
                        ->exists();

                    if ($exists) {
                        $fail(__('The discount code has already been taken.'));
                    }
                },
            ],
            'value_type' => [$creating ? 'required' : 'sometimes', Rule::in(['fixed', 'percent', 'free_shipping'])],
            'value_amount' => $this->valueAmountRules($creating, $valueType),
            'starts_at' => ['sometimes', 'nullable', 'date'],
            'ends_at' => ['sometimes', 'nullable', 'date'],
            'usage_limit' => ['sometimes', 'nullable', 'integer', 'min:1'],
            'status' => ['sometimes', Rule::in(['draft', 'active', 'expired', 'disabled'])],
            'rules_json' => ['sometimes', 'array'],
            'rules_json.minimum_purchase_amount' => ['nullable', 'integer', 'min:0'],
            'rules_json.min_purchase_amount' => ['nullable', 'integer', 'min:0'],
            'rules_json.applicable_product_ids' => ['nullable', 'array'],
            'rules_json.applicable_product_ids.*' => ['integer', Rule::exists('products', 'id')->where('store_id', $store->getKey())],
            'rules_json.applicable_collection_ids' => ['nullable', 'array'],
            'rules_json.applicable_collection_ids.*' => ['integer', Rule::exists('collections', 'id')->where('store_id', $store->getKey())],
            'rules_json.customer_eligibility' => ['nullable', Rule::in(['all', 'specific_customers', 'specific_segments'])],
            'rules_json.once_per_customer' => ['nullable', 'boolean'],
            'rules_json.one_per_customer' => ['nullable', 'boolean'],
        ]);

        $this->validateDates($validated, $discount);

        return $validated;
    }

    /**
     * @return list<mixed>
     */
    private function valueAmountRules(bool $creating, string $valueType): array
    {
        $presence = $creating ? 'required' : 'sometimes';

        return match ($valueType) {
            'percent' => [$presence, 'integer', 'min:1', 'max:100'],
            'free_shipping' => ['sometimes', 'nullable', 'integer', 'min:0'],
            default => [$presence, 'integer', 'min:1'],
        };
    }

    /**
     * @param  array<string, mixed>  $validated
     */
    private function validateDates(array $validated, ?Discount $discount): void
    {
        if (! array_key_exists('ends_at', $validated) || $validated['ends_at'] === null) {
            return;
        }

        $startsAt = data_get($validated, 'starts_at', $discount?->starts_at ?? now());

        if (! $startsAt instanceof CarbonInterface) {
            $startsAt = now()->parse($startsAt);
        }

        $endsAt = now()->parse($validated['ends_at']);

        if ($endsAt->lte($startsAt)) {
            throw ValidationException::withMessages([
                'ends_at' => __('The end date must be after the start date.'),
            ]);
        }
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForCreate(array $validated, Store $store): array
    {
        $type = (string) $validated['type'];
        $valueType = (string) $validated['value_type'];

        return [
            'store_id' => $store->getKey(),
            'type' => $type,
            'code' => $type === 'code' ? Str::upper(trim((string) $validated['code'])) : null,
            'value_type' => $valueType,
            'value_amount' => $valueType === 'free_shipping' ? 0 : (int) $validated['value_amount'],
            'starts_at' => $validated['starts_at'] ?? now(),
            'ends_at' => $validated['ends_at'] ?? null,
            'usage_limit' => $validated['usage_limit'] ?? null,
            'usage_count' => 0,
            'rules_json' => $this->rulesPayload($validated['rules_json'] ?? []),
            'status' => $validated['status'] ?? 'active',
        ];
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForUpdate(array $validated, Discount $discount): array
    {
        $attributes = [];
        $valueType = (string) data_get($validated, 'value_type', $discount->value_type->value);

        foreach (['type', 'value_type', 'starts_at', 'ends_at', 'usage_limit', 'status'] as $field) {
            if (array_key_exists($field, $validated)) {
                $attributes[$field] = $validated[$field];
            }
        }

        if (array_key_exists('value_amount', $validated)) {
            $attributes['value_amount'] = $valueType === 'free_shipping' ? 0 : (int) $validated['value_amount'];
        } elseif (array_key_exists('value_type', $validated) && $valueType === 'free_shipping') {
            $attributes['value_amount'] = 0;
        }

        if (array_key_exists('rules_json', $validated)) {
            $attributes['rules_json'] = $this->rulesPayload($validated['rules_json']);
        }

        return $attributes;
    }

    /**
     * @param  array<string, mixed>  $rules
     * @return array<string, mixed>
     */
    private function rulesPayload(array $rules): array
    {
        return [
            'min_purchase_amount' => (int) data_get($rules, 'min_purchase_amount', data_get($rules, 'minimum_purchase_amount', 0)),
            'applicable_product_ids' => $this->integerList(data_get($rules, 'applicable_product_ids', [])),
            'applicable_collection_ids' => $this->integerList(data_get($rules, 'applicable_collection_ids', [])),
            'customer_eligibility' => data_get($rules, 'customer_eligibility', 'all'),
            'one_per_customer' => (bool) data_get($rules, 'one_per_customer', data_get($rules, 'once_per_customer', false)),
        ];
    }

    /**
     * @return list<int>
     */
    private function integerList(mixed $value): array
    {
        return collect(is_array($value) ? $value : [])
            ->map(fn (mixed $id): int => (int) $id)
            ->unique()
            ->values()
            ->all();
    }
}


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


Input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/app/Http/Resources/Admin/V1/DiscountResource.php
*** End Patch


Success. Updated the following files:
D /Users/fabianwesner/Herd/shop/app/Http/Resources/Admin/V1/DiscountResource.php

<?php

namespace App\Http\Resources\Admin\V1;

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

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


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


Input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/app/Http/Resources/Admin/V1/DiscountResource.php
+<?php
+
+namespace App\Http\Resources\Admin\V1;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class DiscountResource 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,
+            'type' => $this->type?->value,
+            'code' => $this->code,
+            'value_type' => $this->value_type?->value,
+            'value_amount' => $this->value_amount,
+            'starts_at' => $this->starts_at?->toIso8601String(),
+            'ends_at' => $this->ends_at?->toIso8601String(),
+            'usage_limit' => $this->usage_limit,
+            'usage_count' => $this->usage_count,
+            'rules_json' => $this->rules_json ?? [],
+            'status' => $this->status?->value,
+            'created_at' => $this->created_at?->toIso8601String(),
+            'updated_at' => $this->updated_at?->toIso8601String(),
+        ];
+    }
+}
*** End Patch


Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Http/Resources/Admin/V1/DiscountResource.php

<?php

namespace App\Http\Resources\Admin\V1;

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

class DiscountResource 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,
            'type' => $this->type?->value,
            'code' => $this->code,
            'value_type' => $this->value_type?->value,
            'value_amount' => $this->value_amount,
            'starts_at' => $this->starts_at?->toIso8601String(),
            'ends_at' => $this->ends_at?->toIso8601String(),
            'usage_limit' => $this->usage_limit,
            'usage_count' => $this->usage_count,
            'rules_json' => $this->rules_json ?? [],
            'status' => $this->status?->value,
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


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


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/routes/api.php
@@
 use App\Http\Controllers\Api\Admin\V1\CollectionController as AdminCollectionController;
 use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
+use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
@@
         Route::middleware('admin.api:write-collections')->group(function (): void {
             Route::post('collections', [AdminCollectionController::class, 'store'])->name('collections.store');
             Route::put('collections/{collection}', [AdminCollectionController::class, 'update'])->name('collections.update');
             Route::delete('collections/{collection}', [AdminCollectionController::class, 'destroy'])->name('collections.destroy');
         });

+        Route::middleware('admin.api:read-discounts')->group(function (): void {
+            Route::get('discounts', [AdminDiscountController::class, 'index'])->name('discounts.index');
+        });
+
+        Route::middleware('admin.api:write-discounts')->group(function (): void {
+            Route::post('discounts', [AdminDiscountController::class, 'store'])->name('discounts.store');
+            Route::put('discounts/{discount}', [AdminDiscountController::class, 'update'])->name('discounts.update');
+            Route::delete('discounts/{discount}', [AdminDiscountController::class, 'destroy'])->name('discounts.destroy');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/routes/api.php

@@ -4,2 +4,3 @@
 use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
+use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
 use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
@@ -75,2 +76,12 @@

+        Route::middleware('admin.api:read-discounts')->group(function (): void {
+            Route::get('discounts', [AdminDiscountController::class, 'index'])->name('discounts.index');
+        });
+
+        Route::middleware('admin.api:write-discounts')->group(function (): void {
+            Route::post('discounts', [AdminDiscountController::class, 'store'])->name('discounts.store');
+            Route::put('discounts/{discount}', [AdminDiscountController::class, 'update'])->name('discounts.update');
+            Route::delete('discounts/{discount}', [AdminDiscountController::class, 'destroy'])->name('discounts.destroy');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {


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


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/DiscountController.php
@@
 use App\Models\Store;
 use Carbon\CarbonInterface;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use Illuminate\Support\Carbon;
 use Illuminate\Support\Str;
@@
-            $startsAt = now()->parse($startsAt);
+            $startsAt = Carbon::parse($startsAt);
         }

-        $endsAt = now()->parse($validated['ends_at']);
+        $endsAt = Carbon::parse($validated['ends_at']);
*** End Patch


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

@@ -13,2 +13,3 @@
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use Illuminate\Support\Carbon;
 use Illuminate\Support\Str;
@@ -190,6 +191,6 @@
         if (! $startsAt instanceof CarbonInterface) {
-            $startsAt = now()->parse($startsAt);
+            $startsAt = Carbon::parse($startsAt);
         }

-        $endsAt = now()->parse($validated['ends_at']);
+        $endsAt = Carbon::parse($validated['ends_at']);


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


sed -n '1,80p' tests/Feature/Api/AdminDiscountApiTest.php


sed -n '1,80p' tests/Feature/Api/AdminDiscountApiTest.php

<?php

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

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


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

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

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


Input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminDiscountApiTest.php
*** End Patch


Success. Updated the following files:
D /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminDiscountApiTest.php

<?php

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

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


Output: {"output":"Success. Updated the following files:\nD /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminDiscountApiTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}