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

# Laravel Best Practices

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

## Consistency First

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

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

## Quick Reference

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## How to Apply

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

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


sed -n '1,220p' database/factories/DiscountFactory.php && sed -n '1435,1455p' specs/07-SEEDERS-AND-TEST-DATA.md


sed -n '1,220p' database/factories/DiscountFactory.php && sed -n '1435,1455p' specs/07-SEEDERS-AND-TEST-DATA.md

DiscountFactory.php

<?php

namespace Database\Factories;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Discount>
 */
class DiscountFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'type' => DiscountType::Code,
            'code' => fake()->unique()->bothify('SAVE##'),
            'value_type' => DiscountValueType::Percent,
            'value_amount' => 10,
            'starts_at' => now()->subDay(),
            'ends_at' => now()->addMonth(),
            'usage_limit' => null,
            'usage_count' => 0,
            'rules_json' => ['customer_eligibility' => 'all'],
            'status' => DiscountStatus::Active,
        ];
    }

    public function fixed(int $amount = 500): static
    {
        return $this->state(fn (array $attributes): array => [
            'value_type' => DiscountValueType::Fixed,
            'value_amount' => $amount,
        ]);
    }

    public function freeShipping(): static
    {
        return $this->state(fn (array $attributes): array => [
            'value_type' => DiscountValueType::FreeShipping,
            'value_amount' => 0,
        ]);
    }
}
### 3.11 DiscountSeeder

**Acme Fashion - 5 discount codes:**

| Code | Type | Value Type | value_amount | starts_at | ends_at | usage_limit | usage_count | rules_json | status |
|------|------|-----------|-------------|-----------|---------|-------------|-------------|-----------|--------|
| WELCOME10 | code | percent | 10 (=10%) | 2025-01-01 | 2027-12-31 | null | 3 | min_purchase_amount: 2000 (=20.00 EUR) | active |
| FLAT5 | code | fixed | 500 (=5.00 EUR) | 2025-01-01 | 2027-12-31 | null | 0 | (empty) | active |
| FREESHIP | code | free_shipping | 0 | 2025-01-01 | 2027-12-31 | null | 1 | (empty) | active |
| EXPIRED20 | code | percent | 20 (=20%) | 2024-01-01 | 2024-12-31 | null | 0 | (empty) | expired |
| MAXED | code | percent | 10 (=10%) | 2025-01-01 | 2027-12-31 | 5 | 5 | (empty) | active |

**Behavioral expectations:**
- WELCOME10: Applies 10% off to orders with subtotal >= 20.00 EUR. Already used 3 times (has capacity remaining since no usage_limit).
- FLAT5: Applies flat 5.00 EUR discount. Never used.
- FREESHIP: Removes shipping cost. Used once.
- EXPIRED20: Expired - checkout must reject this code with "discount expired" error.
- MAXED: Usage limit of 5 reached - checkout must reject this code with "discount limit reached" error.

---


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

namespace Database\Factories;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Discount>
 */
class DiscountFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'type' => DiscountType::Code,
            'code' => fake()->unique()->bothify('SAVE##'),
            'value_type' => DiscountValueType::Percent,
            'value_amount' => 10,
            'starts_at' => now()->subDay(),
            'ends_at' => now()->addMonth(),
            'usage_limit' => null,
            'usage_count' => 0,
            'rules_json' => ['customer_eligibility' => 'all'],
            'status' => DiscountStatus::Active,
        ];
    }

    public function fixed(int $amount = 500): static
    {
        return $this->state(fn (array $attributes): array => [
            'value_type' => DiscountValueType::Fixed,
            'value_amount' => $amount,
        ]);
    }

    public function freeShipping(): static
    {
        return $this->state(fn (array $attributes): array => [
            'value_type' => DiscountValueType::FreeShipping,
            'value_amount' => 0,
        ]);
    }
}
### 3.11 DiscountSeeder

**Acme Fashion - 5 discount codes:**

| Code | Type | Value Type | value_amount | starts_at | ends_at | usage_limit | usage_count | rules_json | status |
|------|------|-----------|-------------|-----------|---------|-------------|-------------|-----------|--------|
| WELCOME10 | code | percent | 10 (=10%) | 2025-01-01 | 2027-12-31 | null | 3 | min_purchase_amount: 2000 (=20.00 EUR) | active |
| FLAT5 | code | fixed | 500 (=5.00 EUR) | 2025-01-01 | 2027-12-31 | null | 0 | (empty) | active |
| FREESHIP | code | free_shipping | 0 | 2025-01-01 | 2027-12-31 | null | 1 | (empty) | active |
| EXPIRED20 | code | percent | 20 (=20%) | 2024-01-01 | 2024-12-31 | null | 0 | (empty) | expired |
| MAXED | code | percent | 10 (=10%) | 2025-01-01 | 2027-12-31 | 5 | 5 | (empty) | active |

**Behavioral expectations:**
- WELCOME10: Applies 10% off to orders with subtotal >= 20.00 EUR. Already used 3 times (has capacity remaining since no usage_limit).
- FLAT5: Applies flat 5.00 EUR discount. Never used.
- FREESHIP: Removes shipping cost. Used once.
- EXPIRED20: Expired - checkout must reject this code with "discount expired" error.
- MAXED: Usage limit of 5 reached - checkout must reject this code with "discount limit reached" error.

---


rg "DiscountStatus|value_type|usage_limit" database/seeders app -n


rg "DiscountStatus|value_type|usage_limit" database/seeders app -n

rg 'DiscountStatus|value_type|usage_limit' database/seeders app -n

database/seeders/DiscountSeeder.php:25:                        'value_type' => $discount['value_type'],
database/seeders/DiscountSeeder.php:29:                        'usage_limit' => null,
database/seeders/DiscountSeeder.php:40:     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
database/seeders/DiscountSeeder.php:45:            ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
database/seeders/DiscountSeeder.php:46:            ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
database/seeders/DiscountSeeder.php:47:            ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
app/Services/DiscountService.php:5:use App\Enums\DiscountStatus;
app/Services/DiscountService.php:48:            ->where('status', DiscountStatus::Active->value)
app/Services/DiscountService.php:65:        if ($discount->status !== DiscountStatus::Active) {
app/Services/DiscountService.php:77:        if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
app/Services/DiscountService.php:78:            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
app/Services/DiscountService.php:82:            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount has already been used by this customer.');
app/Services/DiscountService.php:103:        if ($discount->value_type === DiscountValueType::FreeShipping) {
app/Services/DiscountService.php:114:        $discountAmount = match ($discount->value_type) {
app/Http/Controllers/Api/Admin/V1/DiscountController.php:108:        $valueType = (string) $request->input('value_type', $discount?->value_type->value ?? 'percent');
app/Http/Controllers/Api/Admin/V1/DiscountController.php:141:            'value_type' => [$creating ? 'required' : 'sometimes', Rule::in(['fixed', 'percent', 'free_shipping'])],
app/Http/Controllers/Api/Admin/V1/DiscountController.php:160:            'usage_limit' => ['sometimes', 'nullable', 'integer', 'min:1'],
app/Http/Controllers/Api/Admin/V1/DiscountController.php:198:        $valueType = (string) $validated['value_type'];
app/Http/Controllers/Api/Admin/V1/DiscountController.php:204:            'value_type' => $valueType,
app/Http/Controllers/Api/Admin/V1/DiscountController.php:208:            'usage_limit' => $validated['usage_limit'] ?? null,
app/Http/Controllers/Api/Admin/V1/DiscountController.php:222:        $valueType = (string) data_get($validated, 'value_type', $discount->value_type->value);
app/Http/Controllers/Api/Admin/V1/DiscountController.php:224:        foreach (['type', 'value_type', 'starts_at', 'ends_at', 'usage_limit', 'status'] as $field) {
app/Http/Controllers/Api/Admin/V1/DiscountController.php:232:        } elseif (array_key_exists('value_type', $validated) && $valueType === 'free_shipping') {
app/Models/Discount.php:5:use App\Enums\DiscountStatus;
app/Models/Discount.php:25:        'value_type',
app/Models/Discount.php:29:        'usage_limit',
app/Models/Discount.php:61:            'value_type' => DiscountValueType::class,
app/Models/Discount.php:65:            'usage_limit' => 'integer',
app/Models/Discount.php:68:            'status' => DiscountStatus::class,
app/Enums/DiscountStatus.php:5:enum DiscountStatus: string
app/Http/Resources/Admin/V1/DiscountResource.php:22:            'value_type' => $this->value_type?->value,
app/Http/Resources/Admin/V1/DiscountResource.php:26:            'usage_limit' => $this->usage_limit,
app/Livewire/Admin/Discounts/Form.php:5:use App\Enums\DiscountStatus;
app/Livewire/Admin/Discounts/Form.php:255:        $this->valueType = $discount->value_type->value;
app/Livewire/Admin/Discounts/Form.php:256:        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
app/Livewire/Admin/Discounts/Form.php:268:        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
app/Livewire/Admin/Discounts/Form.php:272:        $this->isActive = $discount->status === DiscountStatus::Active;
app/Livewire/Admin/Discounts/Form.php:284:            'value_type' => DiscountValueType::from($this->valueType),
app/Livewire/Admin/Discounts/Form.php:288:            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
app/Livewire/Admin/Discounts/Form.php:402:    private function statusForPayload(): DiscountStatus
app/Livewire/Admin/Discounts/Form.php:404:        if ($this->discount?->status === DiscountStatus::Expired) {
app/Livewire/Admin/Discounts/Form.php:405:            return DiscountStatus::Expired;
app/Livewire/Admin/Discounts/Form.php:409:            return DiscountStatus::Active;
app/Livewire/Admin/Discounts/Form.php:412:        if ($this->discount?->status === DiscountStatus::Draft || ! $this->discount instanceof Discount) {
app/Livewire/Admin/Discounts/Form.php:413:            return DiscountStatus::Draft;
app/Livewire/Admin/Discounts/Form.php:416:        return DiscountStatus::Disabled;
app/Livewire/Admin/Discounts/Index.php:5:use App\Enums\DiscountStatus;
app/Livewire/Admin/Discounts/Index.php:82:        return match ($discount->value_type) {
app/Livewire/Admin/Discounts/Index.php:91:        if ($discount->status === DiscountStatus::Disabled) {
app/Livewire/Admin/Discounts/Index.php:99:        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
app/Livewire/Admin/Discounts/Index.php:129:                ->where('status', DiscountStatus::Active->value)
app/Livewire/Admin/Discounts/Index.php:136:                    ->where('status', DiscountStatus::Expired->value)
app/Livewire/Admin/Discounts/Index.php:139:                            ->where('status', DiscountStatus::Active->value)
app/Livewire/Admin/Discounts/Index.php:144:                ->where('status', DiscountStatus::Active->value)


Output:
Chunk ID: 5719d8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1538
Output:
database/seeders/DiscountSeeder.php:25:                        'value_type' => $discount['value_type'],
database/seeders/DiscountSeeder.php:29:                        'usage_limit' => null,
database/seeders/DiscountSeeder.php:40:     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
database/seeders/DiscountSeeder.php:45:            ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
database/seeders/DiscountSeeder.php:46:            ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
database/seeders/DiscountSeeder.php:47:            ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
app/Services/DiscountService.php:5:use App\Enums\DiscountStatus;
app/Services/DiscountService.php:48:            ->where('status', DiscountStatus::Active->value)
app/Services/DiscountService.php:65:        if ($discount->status !== DiscountStatus::Active) {
app/Services/DiscountService.php:77:        if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
app/Services/DiscountService.php:78:            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
app/Services/DiscountService.php:82:            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount has already been used by this customer.');
app/Services/DiscountService.php:103:        if ($discount->value_type === DiscountValueType::FreeShipping) {
app/Services/DiscountService.php:114:        $discountAmount = match ($discount->value_type) {
app/Http/Controllers/Api/Admin/V1/DiscountController.php:108:        $valueType = (string) $request->input('value_type', $discount?->value_type->value ?? 'percent');
app/Http/Controllers/Api/Admin/V1/DiscountController.php:141:            'value_type' => [$creating ? 'required' : 'sometimes', Rule::in(['fixed', 'percent', 'free_shipping'])],
app/Http/Controllers/Api/Admin/V1/DiscountController.php:160:            'usage_limit' => ['sometimes', 'nullable', 'integer', 'min:1'],
app/Http/Controllers/Api/Admin/V1/DiscountController.php:198:        $valueType = (string) $validated['value_type'];
app/Http/Controllers/Api/Admin/V1/DiscountController.php:204:            'value_type' => $valueType,
app/Http/Controllers/Api/Admin/V1/DiscountController.php:208:            'usage_limit' => $validated['usage_limit'] ?? null,
app/Http/Controllers/Api/Admin/V1/DiscountController.php:222:        $valueType = (string) data_get($validated, 'value_type', $discount->value_type->value);
app/Http/Controllers/Api/Admin/V1/DiscountController.php:224:        foreach (['type', 'value_type', 'starts_at', 'ends_at', 'usage_limit', 'status'] as $field) {
app/Http/Controllers/Api/Admin/V1/DiscountController.php:232:        } elseif (array_key_exists('value_type', $validated) && $valueType === 'free_shipping') {
app/Models/Discount.php:5:use App\Enums\DiscountStatus;
app/Models/Discount.php:25:        'value_type',
app/Models/Discount.php:29:        'usage_limit',
app/Models/Discount.php:61:            'value_type' => DiscountValueType::class,
app/Models/Discount.php:65:            'usage_limit' => 'integer',
app/Models/Discount.php:68:            'status' => DiscountStatus::class,
app/Enums/DiscountStatus.php:5:enum DiscountStatus: string
app/Http/Resources/Admin/V1/DiscountResource.php:22:            'value_type' => $this->value_type?->value,
app/Http/Resources/Admin/V1/DiscountResource.php:26:            'usage_limit' => $this->usage_limit,
app/Livewire/Admin/Discounts/Form.php:5:use App\Enums\DiscountStatus;
app/Livewire/Admin/Discounts/Form.php:255:        $this->valueType = $discount->value_type->value;
app/Livewire/Admin/Discounts/Form.php:256:        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
app/Livewire/Admin/Discounts/Form.php:268:        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
app/Livewire/Admin/Discounts/Form.php:272:        $this->isActive = $discount->status === DiscountStatus::Active;
app/Livewire/Admin/Discounts/Form.php:284:            'value_type' => DiscountValueType::from($this->valueType),
app/Livewire/Admin/Discounts/Form.php:288:            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
app/Livewire/Admin/Discounts/Form.php:402:    private function statusForPayload(): DiscountStatus
app/Livewire/Admin/Discounts/Form.php:404:        if ($this->discount?->status === DiscountStatus::Expired) {
app/Livewire/Admin/Discounts/Form.php:405:            return DiscountStatus::Expired;
app/Livewire/Admin/Discounts/Form.php:409:            return DiscountStatus::Active;
app/Livewire/Admin/Discounts/Form.php:412:        if ($this->discount?->status === DiscountStatus::Draft || ! $this->discount instanceof Discount) {
app/Livewire/Admin/Discounts/Form.php:413:            return DiscountStatus::Draft;
app/Livewire/Admin/Discounts/Form.php:416:        return DiscountStatus::Disabled;
app/Livewire/Admin/Discounts/Index.php:5:use App\Enums\DiscountStatus;
app/Livewire/Admin/Discounts/Index.php:82:        return match ($discount->value_type) {
app/Livewire/Admin/Discounts/Index.php:91:        if ($discount->status === DiscountStatus::Disabled) {
app/Livewire/Admin/Discounts/Index.php:99:        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
app/Livewire/Admin/Discounts/Index.php:129:                ->where('status', DiscountStatus::Active->value)
app/Livewire/Admin/Discounts/Index.php:136:                    ->where('status', DiscountStatus::Expired->value)
app/Livewire/Admin/Discounts/Index.php:139:                            ->where('status', DiscountStatus::Active->value)
app/Livewire/Admin/Discounts/Index.php:144:                ->where('status', DiscountStatus::Active->value)


rg "Discount is not active|Discount has expired|expired|usage limit" tests -n


rg "Discount is not active|Discount has expired|expired|usage limit" tests -n

rg 'Discount is not active|Discount has expired|expired|usage limit' tests -n

Question: Discount is not active|Discount has expired|expired|usage limit

tests/Feature/Api/AdminOrderApiTest.php:212:test('admin order api rejects expired bearer tokens', function (): void {
tests/Feature/Checkout/CheckoutServiceTest.php:111:    $expired = app(CheckoutService::class)->expireCheckout($checkout);
tests/Feature/Checkout/CheckoutServiceTest.php:112:    expect($expired->status)->toBe(CheckoutStatus::Expired)
tests/Feature/Admin/DiscountManagementTest.php:142:        ->set('statusFilter', 'expired')
tests/Feature/Admin/DiscountManagementTest.php:239:test('admin discount form creates drafts by default and preserves expired discounts', function (): void {
tests/Feature/Admin/DiscountManagementTest.php:258:    $expired = Discount::factory()->create([
tests/Feature/Admin/DiscountManagementTest.php:267:        ->test(AdminDiscountForm::class, ['discount' => $expired])
tests/Feature/Admin/DiscountManagementTest.php:275:        ->and($expired->refresh()->status)->toBe(DiscountStatus::Expired);


Output:
Chunk ID: 841f0b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 235
Output:
tests/Feature/Api/AdminOrderApiTest.php:212:test('admin order api rejects expired bearer tokens', function (): void {
tests/Feature/Checkout/CheckoutServiceTest.php:111:    $expired = app(CheckoutService::class)->expireCheckout($checkout);
tests/Feature/Checkout/CheckoutServiceTest.php:112:    expect($expired->status)->toBe(CheckoutStatus::Expired)
tests/Feature/Admin/DiscountManagementTest.php:142:        ->set('statusFilter', 'expired')
tests/Feature/Admin/DiscountManagementTest.php:239:test('admin discount form creates drafts by default and preserves expired discounts', function (): void {
tests/Feature/Admin/DiscountManagementTest.php:258:    $expired = Discount::factory()->create([
tests/Feature/Admin/DiscountManagementTest.php:267:        ->test(AdminDiscountForm::class, ['discount' => $expired])
tests/Feature/Admin/DiscountManagementTest.php:275:        ->and($expired->refresh()->status)->toBe(DiscountStatus::Expired);


Input:
*** Begin Patch
*** Update File: database/seeders/DiscountSeeder.php
@@
                     [
                         'type' => 'code',
                         'value_type' => $discount['value_type'],
                         'value_amount' => $discount['value_amount'],
-                        'starts_at' => now()->subDay(),
-                        'ends_at' => now()->addMonth(),
-                        'usage_limit' => null,
-                        'usage_count' => 0,
+                        'starts_at' => $discount['starts_at'] ?? now()->subDay(),
+                        'ends_at' => $discount['ends_at'] ?? now()->addMonth(),
+                        'usage_limit' => $discount['usage_limit'] ?? null,
+                        'usage_count' => $discount['usage_count'] ?? 0,
                         'rules_json' => $discount['rules_json'],
-                        'status' => 'active',
+                        'status' => $discount['status'] ?? 'active',
                     ],
                 );
             }
         });
@@
-     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
+     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>, starts_at?: \Illuminate\Support\Carbon, ends_at?: \Illuminate\Support\Carbon, usage_limit?: int|null, usage_count?: int, status?: string}>
      */
     private function discounts(): array
     {
         return [
+            ['code' => 'WELCOME10', 'value_type' => 'percent', 'value_amount' => 10, 'usage_count' => 3, 'rules_json' => ['min_purchase_amount' => 2000]],
+            ['code' => 'FLAT5', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => []],
             ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
             ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
             ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
+            ['code' => 'EXPIRED20', 'value_type' => 'percent', 'value_amount' => 20, 'ends_at' => now()->subDay(), 'rules_json' => [], 'status' => 'expired'],
+            ['code' => 'MAXED', 'value_type' => 'percent', 'value_amount' => 10, 'usage_limit' => 5, 'usage_count' => 5, 'rules_json' => []],
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M database/seeders/DiscountSeeder.php

@@ -26,8 +26,8 @@
                         'value_amount' => $discount['value_amount'],
-                        'starts_at' => now()->subDay(),
-                        'ends_at' => now()->addMonth(),
-                        'usage_limit' => null,
-                        'usage_count' => 0,
+                        'starts_at' => $discount['starts_at'] ?? now()->subDay(),
+                        'ends_at' => $discount['ends_at'] ?? now()->addMonth(),
+                        'usage_limit' => $discount['usage_limit'] ?? null,
+                        'usage_count' => $discount['usage_count'] ?? 0,
                         'rules_json' => $discount['rules_json'],
-                        'status' => 'active',
+                        'status' => $discount['status'] ?? 'active',
                     ],
@@ -39,3 +39,3 @@
     /**
-     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
+     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>, starts_at?: \Illuminate\Support\Carbon, ends_at?: \Illuminate\Support\Carbon, usage_limit?: int|null, usage_count?: int, status?: string}>
      */
@@ -44,2 +44,4 @@
         return [
+            ['code' => 'WELCOME10', 'value_type' => 'percent', 'value_amount' => 10, 'usage_count' => 3, 'rules_json' => ['min_purchase_amount' => 2000]],
+            ['code' => 'FLAT5', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => []],
             ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
@@ -47,2 +49,4 @@
             ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
+            ['code' => 'EXPIRED20', 'value_type' => 'percent', 'value_amount' => 20, 'ends_at' => now()->subDay(), 'rules_json' => [], 'status' => 'expired'],
+            ['code' => 'MAXED', 'value_type' => 'percent', 'value_amount' => 10, 'usage_limit' => 5, 'usage_count' => 5, 'rules_json' => []],
         ];


Output: {"output":"Success. Updated the following files:\nM database/seeders/DiscountSeeder.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Services/DiscountService.php
@@
     private function validateDiscountForCart(Discount $discount, Cart $cart): void
     {
-        if ($discount->status !== DiscountStatus::Active) {
-            throw InvalidDiscountException::because('discount_expired', 'Discount is not active.');
-        }
-
-        if ($discount->starts_at->isFuture()) {
-            throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
-        }
-
-        if ($discount->ends_at !== null && $discount->ends_at->isPast()) {
+        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
             throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
         }

+        if ($discount->status !== DiscountStatus::Active) {
+            throw InvalidDiscountException::because('discount_not_active', 'Discount is not active.');
+        }
+
+        if ($discount->starts_at->isFuture()) {
+            throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
+        }
+
         if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
             throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
         }
*** End Patch


Success. Updated the following files:
M app/Services/DiscountService.php

@@ -64,4 +64,8 @@
     {
+        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
+            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
+        }
+
         if ($discount->status !== DiscountStatus::Active) {
-            throw InvalidDiscountException::because('discount_expired', 'Discount is not active.');
+            throw InvalidDiscountException::because('discount_not_active', 'Discount is not active.');
         }
@@ -70,6 +74,2 @@
             throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
-        }
-
-        if ($discount->ends_at !== null && $discount->ends_at->isPast()) {
-            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
         }


Output: {"output":"Success. Updated the following files:\nM app/Services/DiscountService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


The seed/service changes are small: the spec discount codes now exist, while an expired status produces the expected expired message. I’m creating the Suite 8 browser test file next and will use it to validate the whole cart flow against those fixtures.


php artisan make:test Browser/Storefront/CartTest --pest --no-interaction


Output:
Chunk ID: 201ec0
Wall time: 0.1866 seconds
Process exited with code 0
Original token count: 22
Output:

   INFO  Test [tests/Feature/Browser/Storefront/CartTest.php] created successfully.


php artisan make:test Browser/Storefront/CartTest --pest --no-interaction

INFO  Test [tests/Feature/Browser/Storefront/CartTest.php] created successfully.


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


/bin/zsh
-lc
rmdir tests/Feature/Browser/Storefront


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


/bin/zsh
-lc
rmdir tests/Feature/Browser


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


rg "toast|flux:toast|Added to cart|data-test" resources app tests -n


rg "toast|flux:toast|Added to cart|data-test" resources app tests -n

rg 'toast|flux:toast|Added to cart|data-test' resources app tests -n

Question: toast|flux:toast|Added to cart|data-test

tests/Browser/Admin/AuthenticationTest.php:94:        ->click('button[data-test="logout-button"]:visible')
app/Livewire/Admin/Developers/Index.php:70:        $this->dispatch('toast', type: 'success', message: __('API token generated'));
app/Livewire/Admin/Developers/Index.php:80:        $this->dispatch('toast', type: 'success', message: __('API token revoked'));
app/Livewire/Admin/Developers/Index.php:127:        $this->dispatch('toast', type: 'success', message: __('Webhook saved'));
app/Livewire/Admin/Developers/Index.php:135:        $this->dispatch('toast', type: 'success', message: __('Webhook deleted'));
resources/views/layouts/app/sidebar.blade.php:148:                            data-test="logout-button"
app/Livewire/Admin/Themes/Editor.php:89:        $this->dispatch('toast', type: 'success', message: __('Theme file saved'));
app/Livewire/Admin/Themes/Editor.php:107:        $this->dispatch('toast', type: 'success', message: __('Theme saved'));
app/Livewire/Admin/Themes/Index.php:59:        $this->dispatch('toast', type: 'success', message: __('Theme published'));
app/Livewire/Admin/Apps/Show.php:48:        $this->dispatch('toast', type: 'success', message: __('App uninstalled'));
app/Livewire/Admin/Apps/Index.php:38:        $this->dispatch('toast', type: 'success', message: __('App uninstalled'));
app/Livewire/Admin/Pages/Form.php:104:        $this->dispatch('toast', type: 'success', message: __('Page saved'));
app/Livewire/Admin/Collections/Form.php:110:        $this->dispatch('toast', type: 'success', message: __('Collection saved'));
app/Livewire/Admin/Collections/Index.php:33:        $this->dispatch('toast', type: 'success', message: __('Collection deleted'));
app/Livewire/Admin/Products/Form.php:236:        $this->dispatch('toast', type: 'success', message: __('Product saved'));
app/Livewire/Admin/Products/Form.php:254:        $this->dispatch('toast', type: 'success', message: __('Media uploaded'));
app/Livewire/Admin/Products/Index.php:143:        $this->dispatch('toast', type: 'success', message: __('Products updated'));
resources/views/livewire/admin/products/form.blade.php:48:                            data-test="product-media-input"
resources/views/livewire/admin/products/form.blade.php:52:                            <flux:button type="button" wire:click="uploadMedia" variant="primary" icon="cloud-arrow-up" wire:loading.attr="disabled" wire:target="newMedia,uploadMedia" data-test="product-media-upload-button">
app/Livewire/Admin/Orders/Show.php:68:            $this->dispatch('toast', type: 'success', message: __('Payment confirmed'));
app/Livewire/Admin/Orders/Show.php:96:            $this->dispatch('toast', type: 'success', message: __('Refund processed'));
app/Livewire/Admin/Orders/Show.php:136:            $this->dispatch('toast', type: 'success', message: __('Fulfillment created'));
app/Livewire/Admin/Orders/Show.php:148:            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as shipped'));
app/Livewire/Admin/Orders/Show.php:160:            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as delivered'));
app/Livewire/Admin/Discounts/Form.php:137:        $this->dispatch('toast', type: 'success', message: __('Discount saved'));
app/Livewire/Admin/Settings/Notifications.php:83:        $this->dispatch('toast', type: 'success', message: __('Notification settings saved'));
app/Livewire/Admin/Settings/Checkout.php:87:        $this->dispatch('toast', type: 'success', message: __('Checkout settings saved'));
app/Livewire/Admin/Settings/Shipping.php:115:        $this->dispatch('toast', type: 'success', message: __('Shipping zone saved'));
app/Livewire/Admin/Settings/Shipping.php:192:        $this->dispatch('toast', type: 'success', message: __('Shipping rate saved'));
app/Livewire/Admin/Navigation/Index.php:232:        $this->dispatch('toast', type: 'success', message: __('Navigation saved'));
app/Livewire/Storefront/Products/Show.php:85:        $this->dispatch('toast', type: 'success', message: __('Added to cart'));
app/Livewire/Admin/Settings/Index.php:99:        $this->dispatch('toast', type: 'success', message: __('Settings saved'));
app/Livewire/Admin/Settings/Index.php:133:        $this->dispatch('toast', type: 'success', message: __('Domain added'));
app/Livewire/Admin/Settings/Taxes.php:100:        $this->dispatch('toast', type: 'success', message: __('Tax settings saved'));
app/Livewire/Admin/Search/Settings.php:82:        $this->dispatch('toast', type: 'success', message: __('Search settings saved'));
app/Livewire/Admin/Search/Settings.php:104:        $this->dispatch('toast', type: 'success', message: __('Search index rebuilt'));
app/Livewire/Admin/Customers/Show.php:102:        $this->dispatch('toast', type: 'success', message: __('Address saved'));
app/Livewire/Admin/Customers/Show.php:115:        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
app/Livewire/Admin/Customers/Show.php:130:        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
resources/views/livewire/auth/login.blade.php:46:                <flux:button variant="primary" type="submit" class="w-full" data-test="login-button">
resources/views/livewire/auth/verify-email.blade.php:23:                <flux:button variant="ghost" type="submit" class="text-sm cursor-pointer" data-test="logout-button">
resources/views/livewire/auth/forgot-password.blade.php:21:            <flux:button variant="primary" type="submit" class="w-full" data-test="email-password-reset-link-button">
resources/views/components/desktop-user-menu.blade.php:8:        data-test="sidebar-menu-button"
resources/views/components/desktop-user-menu.blade.php:34:                    data-test="logout-button"
resources/views/livewire/admin/auth/login.blade.php:27:        <flux:button variant="primary" type="submit" class="w-full" data-test="admin-login-button">
resources/views/livewire/auth/confirm-password.blade.php:23:            <flux:button variant="primary" type="submit" class="w-full" data-test="confirm-password-button">
resources/views/livewire/auth/register.blade.php:56:                <flux:button type="submit" variant="primary" class="w-full" data-test="register-user-button">
resources/views/livewire/auth/reset-password.blade.php:46:                <flux:button type="submit" variant="primary" class="w-full" data-test="reset-password-button">
resources/views/livewire/storefront/account/auth/register.blade.php:43:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-register-button">
resources/views/livewire/storefront/account/auth/login.blade.php:31:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-login-button">
resources/views/livewire/storefront/account/auth/forgot-password.blade.php:21:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-forgot-password-button">
resources/views/livewire/storefront/account/auth/reset-password.blade.php:33:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-reset-password-button">


Output:
Chunk ID: ed3057
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1790
Output:
tests/Browser/Admin/AuthenticationTest.php:94:        ->click('button[data-test="logout-button"]:visible')
app/Livewire/Admin/Developers/Index.php:70:        $this->dispatch('toast', type: 'success', message: __('API token generated'));
app/Livewire/Admin/Developers/Index.php:80:        $this->dispatch('toast', type: 'success', message: __('API token revoked'));
app/Livewire/Admin/Developers/Index.php:127:        $this->dispatch('toast', type: 'success', message: __('Webhook saved'));
app/Livewire/Admin/Developers/Index.php:135:        $this->dispatch('toast', type: 'success', message: __('Webhook deleted'));
resources/views/layouts/app/sidebar.blade.php:148:                            data-test="logout-button"
app/Livewire/Admin/Themes/Editor.php:89:        $this->dispatch('toast', type: 'success', message: __('Theme file saved'));
app/Livewire/Admin/Themes/Editor.php:107:        $this->dispatch('toast', type: 'success', message: __('Theme saved'));
app/Livewire/Admin/Themes/Index.php:59:        $this->dispatch('toast', type: 'success', message: __('Theme published'));
app/Livewire/Admin/Apps/Show.php:48:        $this->dispatch('toast', type: 'success', message: __('App uninstalled'));
app/Livewire/Admin/Apps/Index.php:38:        $this->dispatch('toast', type: 'success', message: __('App uninstalled'));
app/Livewire/Admin/Pages/Form.php:104:        $this->dispatch('toast', type: 'success', message: __('Page saved'));
app/Livewire/Admin/Collections/Form.php:110:        $this->dispatch('toast', type: 'success', message: __('Collection saved'));
app/Livewire/Admin/Collections/Index.php:33:        $this->dispatch('toast', type: 'success', message: __('Collection deleted'));
app/Livewire/Admin/Products/Form.php:236:        $this->dispatch('toast', type: 'success', message: __('Product saved'));
app/Livewire/Admin/Products/Form.php:254:        $this->dispatch('toast', type: 'success', message: __('Media uploaded'));
app/Livewire/Admin/Products/Index.php:143:        $this->dispatch('toast', type: 'success', message: __('Products updated'));
resources/views/livewire/admin/products/form.blade.php:48:                            data-test="product-media-input"
resources/views/livewire/admin/products/form.blade.php:52:                            <flux:button type="button" wire:click="uploadMedia" variant="primary" icon="cloud-arrow-up" wire:loading.attr="disabled" wire:target="newMedia,uploadMedia" data-test="product-media-upload-button">
app/Livewire/Admin/Orders/Show.php:68:            $this->dispatch('toast', type: 'success', message: __('Payment confirmed'));
app/Livewire/Admin/Orders/Show.php:96:            $this->dispatch('toast', type: 'success', message: __('Refund processed'));
app/Livewire/Admin/Orders/Show.php:136:            $this->dispatch('toast', type: 'success', message: __('Fulfillment created'));
app/Livewire/Admin/Orders/Show.php:148:            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as shipped'));
app/Livewire/Admin/Orders/Show.php:160:            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as delivered'));
app/Livewire/Admin/Discounts/Form.php:137:        $this->dispatch('toast', type: 'success', message: __('Discount saved'));
app/Livewire/Admin/Settings/Notifications.php:83:        $this->dispatch('toast', type: 'success', message: __('Notification settings saved'));
app/Livewire/Admin/Settings/Checkout.php:87:        $this->dispatch('toast', type: 'success', message: __('Checkout settings saved'));
app/Livewire/Admin/Settings/Shipping.php:115:        $this->dispatch('toast', type: 'success', message: __('Shipping zone saved'));
app/Livewire/Admin/Settings/Shipping.php:192:        $this->dispatch('toast', type: 'success', message: __('Shipping rate saved'));
app/Livewire/Admin/Navigation/Index.php:232:        $this->dispatch('toast', type: 'success', message: __('Navigation saved'));
app/Livewire/Storefront/Products/Show.php:85:        $this->dispatch('toast', type: 'success', message: __('Added to cart'));
app/Livewire/Admin/Settings/Index.php:99:        $this->dispatch('toast', type: 'success', message: __('Settings saved'));
app/Livewire/Admin/Settings/Index.php:133:        $this->dispatch('toast', type: 'success', message: __('Domain added'));
app/Livewire/Admin/Settings/Taxes.php:100:        $this->dispatch('toast', type: 'success', message: __('Tax settings saved'));
app/Livewire/Admin/Search/Settings.php:82:        $this->dispatch('toast', type: 'success', message: __('Search settings saved'));
app/Livewire/Admin/Search/Settings.php:104:        $this->dispatch('toast', type: 'success', message: __('Search index rebuilt'));
app/Livewire/Admin/Customers/Show.php:102:        $this->dispatch('toast', type: 'success', message: __('Address saved'));
app/Livewire/Admin/Customers/Show.php:115:        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
app/Livewire/Admin/Customers/Show.php:130:        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
resources/views/livewire/auth/login.blade.php:46:                <flux:button variant="primary" type="submit" class="w-full" data-test="login-button">
resources/views/livewire/auth/verify-email.blade.php:23:                <flux:button variant="ghost" type="submit" class="text-sm cursor-pointer" data-test="logout-button">
resources/views/livewire/auth/forgot-password.blade.php:21:            <flux:button variant="primary" type="submit" class="w-full" data-test="email-password-reset-link-button">
resources/views/components/desktop-user-menu.blade.php:8:        data-test="sidebar-menu-button"
resources/views/components/desktop-user-menu.blade.php:34:                    data-test="logout-button"
resources/views/livewire/admin/auth/login.blade.php:27:        <flux:button variant="primary" type="submit" class="w-full" data-test="admin-login-button">
resources/views/livewire/auth/confirm-password.blade.php:23:            <flux:button variant="primary" type="submit" class="w-full" data-test="confirm-password-button">
resources/views/livewire/auth/register.blade.php:56:                <flux:button type="submit" variant="primary" class="w-full" data-test="register-user-button">
resources/views/livewire/auth/reset-password.blade.php:46:                <flux:button type="submit" variant="primary" class="w-full" data-test="reset-password-button">
resources/views/livewire/storefront/account/auth/register.blade.php:43:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-register-button">
resources/views/livewire/storefront/account/auth/login.blade.php:31:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-login-button">
resources/views/livewire/storefront/account/auth/forgot-password.blade.php:21:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-forgot-password-button">
resources/views/livewire/storefront/account/auth/reset-password.blade.php:33:        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-reset-password-button">


rg "Discount code was not found|discount_not_found|Invalid discount code" -n app tests resources specs


rg "Discount code was not found|discount_not_found|Invalid discount code" -n app tests resources specs

rg 'Discount code was not found|discount_not_found|Invalid discount code' -n app tests resources specs

Question: Discount code was not found|discount_not_found|Invalid discount code

specs/05-BUSINESS-LOGIC.md:663:    B -->|No| C["Error: discount_not_found"]
specs/05-BUSINESS-LOGIC.md:683:| `discount_not_found` | Code does not exist in this store |
specs/04-STOREFRONT-UI.md:826:| Error state | Small red text below, e.g., "Invalid discount code" or "This code has expired" |
specs/04-STOREFRONT-UI.md:1916:| Invalid discount code | "Invalid discount code" | error |
specs/08-PLAYWRIGHT-E2E-PLAN.md:1437:    DiscountCheck -->|no: invalid| ErrorInvalid[Error: Invalid discount code]
specs/08-PLAYWRIGHT-E2E-PLAN.md:1452:| INVALID | n/a | n/a | n/a | does not exist | Rejected with "Invalid discount code" error |
specs/08-PLAYWRIGHT-E2E-PLAN.md:1588:- Page displays "Invalid discount code"
tests/Feature/Api/StorefrontCheckoutApiTest.php:193:        ->assertJsonPath('reason', 'discount_not_found');
app/Services/DiscountService.php:32:            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');


Output:
Chunk ID: fd2e57
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 241
Output:
specs/05-BUSINESS-LOGIC.md:663:    B -->|No| C["Error: discount_not_found"]
specs/05-BUSINESS-LOGIC.md:683:| `discount_not_found` | Code does not exist in this store |
specs/04-STOREFRONT-UI.md:826:| Error state | Small red text below, e.g., "Invalid discount code" or "This code has expired" |
specs/04-STOREFRONT-UI.md:1916:| Invalid discount code | "Invalid discount code" | error |
specs/08-PLAYWRIGHT-E2E-PLAN.md:1437:    DiscountCheck -->|no: invalid| ErrorInvalid[Error: Invalid discount code]
specs/08-PLAYWRIGHT-E2E-PLAN.md:1452:| INVALID | n/a | n/a | n/a | does not exist | Rejected with "Invalid discount code" error |
specs/08-PLAYWRIGHT-E2E-PLAN.md:1588:- Page displays "Invalid discount code"
tests/Feature/Api/StorefrontCheckoutApiTest.php:193:        ->assertJsonPath('reason', 'discount_not_found');
app/Services/DiscountService.php:32:            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');


A few small implementation gaps are worth fixing before the browser tests: the cart heading should match the spec, the invalid-code copy should be customer-facing, and discount placeholders should point at the seeded spec code. I’m applying those, then I’ll write the Suite 8 tests.


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/fluxui-development/SKILL.md


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/fluxui-development/SKILL.md

---
name: fluxui-development
description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with <flux:*> components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling."
license: MIT
metadata:
  author: laravel
---

# Flux UI Development

## Documentation

Use `search-docs` for detailed Flux UI patterns and documentation.

## Basic Usage

This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components.

Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize.

Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs.

<!-- Basic Button -->
```blade
<flux:button variant="primary">Click me</flux:button>
```

## Available Components (Free Edition)

Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip

## Icons

Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names.

<!-- Icon Button -->
```blade
<flux:button icon="arrow-down-tray">Export</flux:button>
```

For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command:

```bash
php artisan flux:icon crown grip-vertical github
```

## Common Patterns

### Form Fields

<!-- Form Field -->
```blade
<flux:field>
    <flux:label>Email</flux:label>
    <flux:input type="email" wire:model="email" />
    <flux:error name="email" />
</flux:field>
```

### Modals

<!-- Modal -->
```blade
<flux:modal wire:model="showModal">
    <flux:heading>Title</flux:heading>
    <p>Content</p>
</flux:modal>
```

## Verification

1. Check component renders correctly
2. Test interactive states
3. Verify mobile responsiveness

## Common Pitfalls

- Trying to use Pro-only components in the free edition
- Not checking if a Flux component exists before creating custom implementations
- Forgetting to use the `search-docs` tool for component-specific documentation
- Not following existing project patterns for Flux usage


Output:
Chunk ID: 47fe4c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 675
Output:
---
name: fluxui-development
description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with <flux:*> components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling."
license: MIT
metadata:
  author: laravel
---

# Flux UI Development

## Documentation

Use `search-docs` for detailed Flux UI patterns and documentation.

## Basic Usage

This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components.

Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize.

Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs.

<!-- Basic Button -->
```blade
<flux:button variant="primary">Click me</flux:button>
```

## Available Components (Free Edition)

Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip

## Icons

Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names.

<!-- Icon Button -->
```blade
<flux:button icon="arrow-down-tray">Export</flux:button>
```

For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command:

```bash
php artisan flux:icon crown grip-vertical github
```

## Common Patterns

### Form Fields

<!-- Form Field -->
```blade
<flux:field>
    <flux:label>Email</flux:label>
    <flux:input type="email" wire:model="email" />
    <flux:error name="email" />
</flux:field>
```

### Modals

<!-- Modal -->
```blade
<flux:modal wire:model="showModal">
    <flux:heading>Title</flux:heading>
    <p>Content</p>
</flux:modal>
```

## Verification

1. Check component renders correctly
2. Test interactive states
3. Verify mobile responsiveness

## Common Pitfalls

- Trying to use Pro-only components in the free edition
- Not checking if a Flux component exists before creating custom implementations
- Forgetting to use the `search-docs` tool for component-specific documentation
- Not following existing project patterns for Flux usage


input placeholder error component
button submit form component


# Search Results

## Wire Submit
Wire Submit | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-submit

Livewire makes it easy to handle form submissions via the `wire:submit` directive. By adding `wire:submit` to a `<form>` element, Livewire will intercept the form submission, prevent the default browser handling, and call any Livewire component method.

Here's a basic example of using `wire:submit` to handle a "Create Post" form submission:

```php
<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public $title = '';

    public $content = '';

    public function save()
    {
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
        ]);

        $this->redirect('/posts');
    }

    public function render()
    {
        return view('livewire.create-post');
    }
}
```

```blade
<form wire:submit="save"> <!-- [tl! highlight] -->
    <input type="text" wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>
</form>
```

In the above example, when a user submits the form by clicking "Save", `wire:submit` intercepts the `submit` event and calls the `save()` action on the server.

> [!info] Livewire automatically calls `preventDefault()`
> `wire:submit` is different than other Livewire event handlers in that it internally calls `event.preventDefault()` without the need for the `.prevent` modifier. This is because there are very few instances you would be listening for the `submit` event and NOT want to prevent it's default browser handling (performing a full form submission to an endpoint).

> [!info] Livewire automatically disables forms while submitting
> By default, when Livewire is sending a form submission to the server, it will disable form submit buttons and mark all form inputs as `readonly`. This way a user cannot submit the same form again until the initial submission is complete.

---

## Components Input > flux:input
Components Input | livewire/flux@2.x | Source: https://fluxui.dev/components/input#livewire/flux

flux:input

<ui-table-scroll-area>

Prop

Description

    wire:model

Binds the input to a Livewire property. See the wire:model documentation for more information.

    label

Label text displayed above the input. When provided, wraps the input in a flux:field component with an adjacent flux:label component. See the field component.

    description

Help text displayed above the input. When provided alongside label, appears between the label and input within the flux:field wrapper. See the field component.

    description:trailing

Help text displayed below the input. When provided alongside label, appears below the label and input within the flux:field wrapper. See the field component.

    placeholder

Placeholder text displayed when the input is empty.

    size

Size of the input. Options: sm, xs.

    variant

Visual style variant. Options: filled. Default: outline.

    disabled

Prevents user interaction with the input.

    readonly

Makes the input read-only.

    invalid

Applies error styling to the input.

    multiple

For file inputs, allows selecting multiple files.

    mask

Input mask pattern using Alpine's mask plugin. Example: 99/99/9999.

    mask:dynamic

Dynamic input mask pattern using Alpine's mask plugin. Example: $money($input).

    icon

Name of the icon displayed at the start of the input.

    icon:trailing

Name of the icon displayed at the end of the input.

    kbd

Keyboard shortcut hint displayed at the end of the input.

    clearable

If true, displays a clear button when the input has content.

    copyable

If true, displays a copy button to copy the input's content (https only).

    viewable

For password inputs, displays a toggle to show/hide the password.

    as

Render the input as a different element. Options: button. Default: input.

    input:class

CSS classes applied directly to the input element instead of the wrapper.

</ui-table-scroll-area>

<ui-table-scroll-area>

Slot

Description

    icon

Custom content displayed at the start of the input (e.g., icons).

    icon:leading

Custom content displayed at the start of the input (e.g., icons).

    icon:trailing

Custom content displayed at the end of the input (e.g., buttons).

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

    data-flux-input

Applied to the root element for styling and identification.

</ui-table-scroll-area>

_[Truncated]_


Output:
Wall time: 2.7991 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Wire Submit\nWire Submit | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-submit\n\nLivewire makes it easy to handle form submissions via the `wire:submit` directive. By adding `wire:submit` to a `<form>` element, Livewire will intercept the form submission, prevent the default browser handling, and call any Livewire component method.\n\nHere's a basic example of using `wire:submit` to handle a \"Create Post\" form submission:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nclass CreatePost extends Component\n{\n    public $title = '';\n\n    public $content = '';\n\n    public function save()\n    {\n        Post::create([\n            'title' => $this->title,\n            'content' => $this->content,\n        ]);\n\n        $this->redirect('/posts');\n    }\n\n    public function render()\n    {\n        return view('livewire.create-post');\n    }\n}\n```\n\n```blade\n<form wire:submit=\"save\"> <!-- [tl! highlight] -->\n    <input type=\"text\" wire:model=\"title\">\n\n    <textarea wire:model=\"content\"></textarea>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nIn the above example, when a user submits the form by clicking \"Save\", `wire:submit` intercepts the `submit` event and calls the `save()` action on the server.\n\n> [!info] Livewire automatically calls `preventDefault()`\n> `wire:submit` is different than other Livewire event handlers in that it internally calls `event.preventDefault()` without the need for the `.prevent` modifier. This is because there are very few instances you would be listening for the `submit` event and NOT want to prevent it's default browser handling (performing a full form submission to an endpoint).\n\n> [!info] Livewire automatically disables forms while submitting\n> By default, when Livewire is sending a form submission to the server, it will disable form submit buttons and mark all form inputs as `readonly`. This way a user cannot submit the same form again until the initial submission is complete.\n\n---\n\n## Components Input > flux:input\nComponents Input | livewire/flux@2.x | Source: https://fluxui.dev/components/input#livewire/flux\n\n
flux:input

<ui-table-scroll-area>

Prop

Description

\n    wire:model\n

Binds the input to a Livewire property. See the wire:model documentation for more information.

\n    label\n

Label text displayed above the input. When provided, wraps the input in a flux:field component with an adjacent flux:label component. See the field component.

\n    description\n

Help text displayed above the input. When provided alongside label, appears between the label and input within the flux:field wrapper. See the field component.

\n    description:trailing\n

Help text displayed below the input. When provided alongside label, appears below the label and input within the flux:field wrapper. See the field component.

\n    placeholder\n

Placeholder text displayed when the input is empty.

\n    size\n

Size of the input. Options: sm, xs.

\n    variant\n

Visual style variant. Options: filled. Default: outline.

\n    disabled\n

Prevents user interaction with the input.

\n    readonly\n

Makes the input read-only.

\n    invalid\n

Applies error styling to the input.

\n    multiple\n

For file inputs, allows selecting multiple files.

\n    mask\n

Input mask pattern using Alpine's mask plugin. Example: 99/99/9999.

\n    mask:dynamic\n

Dynamic input mask pattern using Alpine's mask plugin. Example: $money($input).

\n    icon\n

Name of the icon displayed at the start of the input.

\n    icon:trailing\n

Name of the icon displayed at the end of the input.

\n    kbd\n

Keyboard shortcut hint displayed at the end of the input.

\n    clearable\n

If true, displays a clear button when the input has content.

\n    copyable\n

If true, displays a copy button to copy the input's content (https only).

\n    viewable\n

For password inputs, displays a toggle to show/hide the password.

\n    as\n

Render the input as a different element. Options: button. Default: input.

\n    input:class\n

CSS classes applied directly to the input element instead of the wrapper.

</ui-table-scroll-area>

<ui-table-scroll-area>

Slot

Description

\n    icon\n

Custom content displayed at the start of the input (e.g., icons).

\n    icon:leading\n

Custom content displayed at the start of the input (e.g., icons).

\n    icon:trailing\n

Custom content displayed at the end of the input (e.g., buttons).

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

\n    data-flux-input\n

Applied to the root element for styling and identification.

</ui-table-scroll-area>

\n\n_[Truncated]_"}]


Input:
*** Begin Patch
*** Update File: app/Services/DiscountService.php
@@
         $discount = Discount::withoutGlobalScopes()
             ->where('store_id', $store->getKey())
             ->where('type', DiscountType::Code)
             ->whereRaw('lower(code) = ?', [$normalizedCode])
             ->first();

         if (! $discount instanceof Discount) {
-            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');
+            throw InvalidDiscountException::because('discount_not_found', 'Invalid discount code.');
         }
*** End Patch


@@ -31,3 +31,3 @@
         if (! $discount instanceof Discount) {
-            throw InvalidDiscountException::because('discount_not_found', 'Discount code was not found.');
+            throw InvalidDiscountException::because('discount_not_found', 'Invalid discount code.');
         }


Output: {"output":"Success. Updated the following files:\nM app/Services/DiscountService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: resources/views/livewire/storefront/cart/show.blade.php
@@
-
Cart

+
Your Cart

@@
-                            <flux:input wire:model="discountCode" label="Discount code" placeholder="SAVE10" />
+                            <flux:input wire:model="discountCode" label="Discount code" placeholder="WELCOME10" />
*** End Patch


Success. Updated the following files:
M resources/views/livewire/storefront/cart/show.blade.php

@@ -7,3 +7,3 @@

-
Cart

+
Your Cart

{{ $lineCount }} {{ Str::plural('item', $lineCount) }}

@@ -108,3 +108,3 @@

-                            <flux:input wire:model="discountCode" label="Discount code" placeholder="SAVE10" />
+                            <flux:input wire:model="discountCode" label="Discount code" placeholder="WELCOME10" />


Output: {"output":"Success. Updated the following files:\nM resources/views/livewire/storefront/cart/show.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: resources/views/livewire/storefront/checkout/show.blade.php
@@
-                                <flux:input wire:model="discountCode" label="Discount code" placeholder="SAVE10" />
+                                <flux:input wire:model="discountCode" label="Discount code" placeholder="WELCOME10" />
*** End Patch


Success. Updated the following files:
M resources/views/livewire/storefront/checkout/show.blade.php

@@ -152,3 +152,3 @@
                             <div class="flex gap-2">
-                                <flux:input wire:model="discountCode" label="Discount code" placeholder="SAVE10" />
+                                <flux:input wire:model="discountCode" label="Discount code" placeholder="WELCOME10" />
                                 <flux:button type="button" wire:click="applyDiscount" variant="ghost" class="mt-6">


Output: {"output":"Success. Updated the following files:\nM resources/views/livewire/storefront/checkout/show.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,80p' tests/Browser/Storefront/CartTest.php


sed -n '1,80p' tests/Browser/Storefront/CartTest.php

<?php

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

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


Output:
Chunk ID: 3f387c
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);
});


rg "Premium Slim Fit Jeans|premium-slim-fit-jeans|pants" -n database/seeders/ProductSeeder.php


rg "Premium Slim Fit Jeans|premium-slim-fit-jeans|pants" -n database/seeders/ProductSeeder.php

rg 'Premium Slim Fit Jeans|premium-slim-fit-jeans|pants' -n database/seeders/ProductSeeder.php

Question: Premium Slim Fit Jeans|premium-slim-fit-jeans|pants

274:            $this->product('Premium Slim Fit Jeans', 'premium-slim-fit-jeans', 'Acme Denim', 'Pants', ['new', 'sale'], ['new-arrivals', 'pants-jeans', 'sale'], '
Slim fit jeans crafted from premium stretch denim. Comfortable all-day wear with a modern silhouette.
', [['Size' => ['28', '30', '32', '34', '36']], ['Color' => ['Blue', 'Black']]], ['price_amount' => 7999, 'compare_at_amount' => 9999, 'weight_g' => 800, 'quantity_on_hand' => 8], 'ACME-JNS'),
281:            $this->product('Cargo Pants', 'cargo-pants', 'Acme Workwear', 'Pants', ['popular'], ['pants-jeans'], '
Utility cargo pants with multiple pockets. Durable cotton twill construction.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Khaki', 'Olive', 'Black']]], ['price_amount' => 5499, 'weight_g' => 700, 'quantity_on_hand' => 14], 'ACME-CARGO'),
282:            $this->product('Chino Shorts', 'chino-shorts', 'Acme Basics', 'Pants', ['new', 'trending'], ['pants-jeans', 'new-arrivals'], '
Tailored chino shorts. Comfortable stretch fabric with a clean silhouette.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Navy', 'Sand']]], ['price_amount' => 3999, 'weight_g' => 350, 'quantity_on_hand' => 16], 'ACME-CHINO'),
283:            $this->product('Wide Leg Trousers', 'wide-leg-trousers', 'Acme Denim', 'Pants', ['sale'], ['pants-jeans', 'sale'], '
Relaxed wide leg trousers with a high waist. Flowing drape in premium woven fabric.
', [['Size' => ['S', 'M', 'L']]], ['price_amount' => 4999, 'compare_at_amount' => 6999, 'weight_g' => 550, 'quantity_on_hand' => 7], 'ACME-WIDE'),


Output:
Chunk ID: 629bb0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 398
Output:
274:            $this->product('Premium Slim Fit Jeans', 'premium-slim-fit-jeans', 'Acme Denim', 'Pants', ['new', 'sale'], ['new-arrivals', 'pants-jeans', 'sale'], '
Slim fit jeans crafted from premium stretch denim. Comfortable all-day wear with a modern silhouette.
', [['Size' => ['28', '30', '32', '34', '36']], ['Color' => ['Blue', 'Black']]], ['price_amount' => 7999, 'compare_at_amount' => 9999, 'weight_g' => 800, 'quantity_on_hand' => 8], 'ACME-JNS'),
281:            $this->product('Cargo Pants', 'cargo-pants', 'Acme Workwear', 'Pants', ['popular'], ['pants-jeans'], '
Utility cargo pants with multiple pockets. Durable cotton twill construction.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Khaki', 'Olive', 'Black']]], ['price_amount' => 5499, 'weight_g' => 700, 'quantity_on_hand' => 14], 'ACME-CARGO'),
282:            $this->product('Chino Shorts', 'chino-shorts', 'Acme Basics', 'Pants', ['new', 'trending'], ['pants-jeans', 'new-arrivals'], '
Tailored chino shorts. Comfortable stretch fabric with a clean silhouette.
', [['Size' => ['30', '32', '34', '36']], ['Color' => ['Navy', 'Sand']]], ['price_amount' => 3999, 'weight_g' => 350, 'quantity_on_hand' => 16], 'ACME-CHINO'),
283:            $this->product('Wide Leg Trousers', 'wide-leg-trousers', 'Acme Denim', 'Pants', ['sale'], ['pants-jeans', 'sale'], '
Relaxed wide leg trousers with a high waist. Flowing drape in premium woven fabric.
', [['Size' => ['S', 'M', 'L']]], ['price_amount' => 4999, 'compare_at_amount' => 6999, 'weight_g' => 550, 'quantity_on_hand' => 7], 'ACME-WIDE'),


Input:
*** Begin Patch
*** Update File: tests/Browser/Storefront/CartTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Pest\Browser\Playwright\Playwright;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    Playwright::setHost('shop.test');
+
+    $this->seed(DatabaseSeeder::class);
+});
+
+afterEach(function (): void {
+    Playwright::setHost(null);
+});
+
+function storefrontCartHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+function storefrontCartAddClassicTShirt(): mixed
+{
+    return visit('/products/classic-cotton-t-shirt', storefrontCartHost())
+        ->click('M')
+        ->wait(1)
+        ->click('Black')
+        ->wait(1)
+        ->click('Add to cart')
+        ->wait(1);
+}
+
+function storefrontCartPageWithClassicTShirt(): mixed
+{
+    return storefrontCartAddClassicTShirt()
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Your Cart')
+        ->assertSee('Classic Cotton T-Shirt');
+}
+
+function storefrontCartApplyDiscount(string $code): mixed
+{
+    return storefrontCartPageWithClassicTShirt()
+        ->fill('input[wire\\:model="discountCode"]', $code)
+        ->click('button:has-text("Apply")')
+        ->wait(1);
+}
+
+test('can add product to cart', function (): void {
+    storefrontCartAddClassicTShirt()
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can view cart with added item', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can update quantity in cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->click('button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
+        ->wait(1)
+        ->assertSee('2 items')
+        ->assertSee('49.98')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can remove item from cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->click('button[aria-label="Remove Classic Cotton T-Shirt"]')
+        ->wait(1)
+        ->assertSee('Your cart is empty')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can add multiple different products', function (): void {
+    storefrontCartAddClassicTShirt()
+        ->navigate('/products/premium-slim-fit-jeans')
+        ->wait(1)
+        ->click('Add to cart')
+        ->wait(1)
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply valid discount code welcome ten', function (): void {
+    storefrontCartApplyDiscount('WELCOME10')
+        ->assertSee('WELCOME10')
+        ->assertSee('Discount')
+        ->assertSee('2.50')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows error for invalid discount code', function (): void {
+    storefrontCartApplyDiscount('INVALID')
+        ->assertSee('Invalid discount code')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows error for expired discount code', function (): void {
+    storefrontCartApplyDiscount('EXPIRED20')
+        ->assertSee('expired')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows error for maxed out discount code', function (): void {
+    storefrontCartApplyDiscount('MAXED')
+        ->assertSee('usage limit')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply free shipping discount', function (): void {
+    storefrontCartApplyDiscount('FREESHIP')
+        ->assertSee('FREESHIP')
+        ->assertSee('Free shipping')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply flat five discount for fixed amount off', function (): void {
+    storefrontCartApplyDiscount('FLAT5')
+        ->assertSee('FLAT5')
+        ->assertSee('5.00')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows subtotal and total in cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->assertSee('Subtotal')
+        ->assertSee('Estimated total')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
 });
*** End Patch


Success. Updated the following files:
M tests/Browser/Storefront/CartTest.php

@@ -2,6 +2,142 @@

-test('example', function () {
-    $response = $this->get('/');
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Pest\Browser\Playwright\Playwright;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    Playwright::setHost('shop.test');
+
+    $this->seed(DatabaseSeeder::class);
+});
+
+afterEach(function (): void {
+    Playwright::setHost(null);
+});
+
+function storefrontCartHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+function storefrontCartAddClassicTShirt(): mixed
+{
+    return visit('/products/classic-cotton-t-shirt', storefrontCartHost())
+        ->click('M')
+        ->wait(1)
+        ->click('Black')
+        ->wait(1)
+        ->click('Add to cart')
+        ->wait(1);
+}
+
+function storefrontCartPageWithClassicTShirt(): mixed
+{
+    return storefrontCartAddClassicTShirt()
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Your Cart')
+        ->assertSee('Classic Cotton T-Shirt');
+}
+
+function storefrontCartApplyDiscount(string $code): mixed
+{
+    return storefrontCartPageWithClassicTShirt()
+        ->fill('input[wire\\:model="discountCode"]', $code)
+        ->click('button:has-text("Apply")')
+        ->wait(1);
+}
+
+test('can add product to cart', function (): void {
+    storefrontCartAddClassicTShirt()
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can view cart with added item', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can update quantity in cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->click('button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
+        ->wait(1)
+        ->assertSee('2 items')
+        ->assertSee('49.98')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can remove item from cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->click('button[aria-label="Remove Classic Cotton T-Shirt"]')
+        ->wait(1)
+        ->assertSee('Your cart is empty')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can add multiple different products', function (): void {
+    storefrontCartAddClassicTShirt()
+        ->navigate('/products/premium-slim-fit-jeans')
+        ->wait(1)
+        ->click('Add to cart')
+        ->wait(1)
+        ->navigate('/cart')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply valid discount code welcome ten', function (): void {
+    storefrontCartApplyDiscount('WELCOME10')
+        ->assertSee('WELCOME10')
+        ->assertSee('Discount')
+        ->assertSee('2.50')
+        ->assertNoJavaScriptErrors();
+});

-    $response->assertStatus(200);
+test('shows error for invalid discount code', function (): void {
+    storefrontCartApplyDiscount('INVALID')
+        ->assertSee('Invalid discount code')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows error for expired discount code', function (): void {
+    storefrontCartApplyDiscount('EXPIRED20')
+        ->assertSee('expired')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows error for maxed out discount code', function (): void {
+    storefrontCartApplyDiscount('MAXED')
+        ->assertSee('usage limit')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply free shipping discount', function (): void {
+    storefrontCartApplyDiscount('FREESHIP')
+        ->assertSee('FREESHIP')
+        ->assertSee('Free shipping')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can apply flat five discount for fixed amount off', function (): void {
+    storefrontCartApplyDiscount('FLAT5')
+        ->assertSee('FLAT5')
+        ->assertSee('5.00')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows subtotal and total in cart', function (): void {
+    storefrontCartPageWithClassicTShirt()
+        ->assertSee('Subtotal')
+        ->assertSee('Estimated total')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
 });


Output: {"output":"Success. Updated the following files:\nM tests/Browser/Storefront/CartTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Output:
Chunk ID: 79297a
Wall time: 0.3311 seconds
Process exited with code 0
Original token count: 5
Output:
{"result":"pass"}


/bin/zsh
-lc
vendor/bin/pint --dirty --format agent


sed -n '1,240p' tests/Browser/Storefront/CartTest.php


sed -n '1,90p' database/seeders/DiscountSeeder.php


sed -n '24,90p' app/Services/DiscountService.php


sed -n '24,90p' app/Services/DiscountService.php

        $discount = Discount::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('type', DiscountType::Code)
            ->whereRaw('lower(code) = ?', [$normalizedCode])
            ->first();

        if (! $discount instanceof Discount) {
            throw InvalidDiscountException::because('discount_not_found', 'Invalid discount code.');
        }

        $this->validateDiscountForCart($discount, $cart);

        return $discount;
    }

    /**
     * @return Collection<int, Discount>
     */
    public function automaticForCart(Store $store, Cart $cart): Collection
    {
        return Discount::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('type', DiscountType::Automatic->value)
            ->where('status', DiscountStatus::Active->value)
            ->orderBy('id')
            ->get()
            ->filter(function (Discount $discount) use ($cart): bool {
                try {
                    $this->validateDiscountForCart($discount, $cart);

                    return true;
                } catch (InvalidDiscountException) {
                    return false;
                }
            })
            ->values();
    }

    private function validateDiscountForCart(Discount $discount, Cart $cart): void
    {
        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
        }

        if ($discount->status !== DiscountStatus::Active) {
            throw InvalidDiscountException::because('discount_not_active', 'Discount is not active.');
        }

        if ($discount->starts_at->isFuture()) {
            throw InvalidDiscountException::because('discount_not_yet_active', 'Discount is not active yet.');
        }

        if ($discount->usage_limit !== null && $discount->usage_count >= $discount->usage_limit) {
            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount usage limit has been reached.');
        }

        if ($this->onePerCustomer($discount) && $this->customerHasUsedDiscount($discount, $cart)) {
            throw InvalidDiscountException::because('discount_usage_limit_reached', 'Discount has already been used by this customer.');
        }

        $lines = $this->cartLines($cart);
        $subtotal = $lines->sum('line_subtotal_amount');
        $minimum = (int) data_get($discount->rules_json, 'min_purchase_amount', data_get($discount->rules_json, 'minimum_purchase', 0));

        if ($minimum > 0 && $subtotal < $minimum) {
            throw InvalidDiscountException::because('discount_min_purchase_not_met', 'Cart does not meet the minimum purchase amount.');


sed -n '1,90p' database/seeders/DiscountSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Discount;
use App\Models\Store;
use Illuminate\Database\Seeder;

class DiscountSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            foreach ($this->discounts() as $discount) {
                Discount::withoutGlobalScopes()->updateOrCreate(
                    [
                        'store_id' => $store->getKey(),
                        'code' => $discount['code'],
                    ],
                    [
                        'type' => 'code',
                        'value_type' => $discount['value_type'],
                        'value_amount' => $discount['value_amount'],
                        'starts_at' => $discount['starts_at'] ?? now()->subDay(),
                        'ends_at' => $discount['ends_at'] ?? now()->addMonth(),
                        'usage_limit' => $discount['usage_limit'] ?? null,
                        'usage_count' => $discount['usage_count'] ?? 0,
                        'rules_json' => $discount['rules_json'],
                        'status' => $discount['status'] ?? 'active',
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>, starts_at?: \Illuminate\Support\Carbon, ends_at?: \Illuminate\Support\Carbon, usage_limit?: int|null, usage_count?: int, status?: string}>
     */
    private function discounts(): array
    {
        return [
            ['code' => 'WELCOME10', 'value_type' => 'percent', 'value_amount' => 10, 'usage_count' => 3, 'rules_json' => ['min_purchase_amount' => 2000]],
            ['code' => 'FLAT5', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => []],
            ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
            ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
            ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
            ['code' => 'EXPIRED20', 'value_type' => 'percent', 'value_amount' => 20, 'ends_at' => now()->subDay(), 'rules_json' => [], 'status' => 'expired'],
            ['code' => 'MAXED', 'value_type' => 'percent', 'value_amount' => 10, 'usage_limit' => 5, 'usage_count' => 5, 'rules_json' => []],
        ];
    }
}


sed -n '1,240p' tests/Browser/Storefront/CartTest.php

<?php

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontCartHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontCartAddClassicTShirt(): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCartHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1);
}

function storefrontCartPageWithClassicTShirt(): mixed
{
    return storefrontCartAddClassicTShirt()
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Your Cart')
        ->assertSee('Classic Cotton T-Shirt');
}

function storefrontCartApplyDiscount(string $code): mixed
{
    return storefrontCartPageWithClassicTShirt()
        ->fill('input[wire\\:model="discountCode"]', $code)
        ->click('button:has-text("Apply")')
        ->wait(1);
}

test('can add product to cart', function (): void {
    storefrontCartAddClassicTShirt()
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});

test('can view cart with added item', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});

test('can update quantity in cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->click('button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('2 items')
        ->assertSee('49.98')
        ->assertNoJavaScriptErrors();
});

test('can remove item from cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->click('button[aria-label="Remove Classic Cotton T-Shirt"]')
        ->wait(1)
        ->assertSee('Your cart is empty')
        ->assertNoJavaScriptErrors();
});

test('can add multiple different products', function (): void {
    storefrontCartAddClassicTShirt()
        ->navigate('/products/premium-slim-fit-jeans')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can apply valid discount code welcome ten', function (): void {
    storefrontCartApplyDiscount('WELCOME10')
        ->assertSee('WELCOME10')
        ->assertSee('Discount')
        ->assertSee('2.50')
        ->assertNoJavaScriptErrors();
});

test('shows error for invalid discount code', function (): void {
    storefrontCartApplyDiscount('INVALID')
        ->assertSee('Invalid discount code')
        ->assertNoJavaScriptErrors();
});

test('shows error for expired discount code', function (): void {
    storefrontCartApplyDiscount('EXPIRED20')
        ->assertSee('expired')
        ->assertNoJavaScriptErrors();
});

test('shows error for maxed out discount code', function (): void {
    storefrontCartApplyDiscount('MAXED')
        ->assertSee('usage limit')
        ->assertNoJavaScriptErrors();
});

test('can apply free shipping discount', function (): void {
    storefrontCartApplyDiscount('FREESHIP')
        ->assertSee('FREESHIP')
        ->assertSee('Free shipping')
        ->assertNoJavaScriptErrors();
});

test('can apply flat five discount for fixed amount off', function (): void {
    storefrontCartApplyDiscount('FLAT5')
        ->assertSee('FLAT5')
        ->assertSee('5.00')
        ->assertNoJavaScriptErrors();
});

test('shows subtotal and total in cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->assertSee('Subtotal')
        ->assertSee('Estimated total')
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});